From 6dfe6a12efe4202adb62d774aa640de9ec953e9f Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 21 Jul 2026 12:20:55 +0200 Subject: [PATCH 1/2] feat(tools): add harness filesystem context --- packages/tools/package.json | 7 + .../__tests__/harness-context-testing.test.ts | 18 + .../src/__tests__/harness-context.test.ts | 52 +++ packages/tools/src/harness-context.ts | 80 +++++ packages/tools/src/index.ts | 1 + packages/tools/src/testing.ts | 34 ++ pnpm-lock.yaml | 307 ++++++++++++++++++ 7 files changed, 499 insertions(+) create mode 100644 packages/tools/src/__tests__/harness-context-testing.test.ts create mode 100644 packages/tools/src/__tests__/harness-context.test.ts create mode 100644 packages/tools/src/harness-context.ts create mode 100644 packages/tools/src/testing.ts diff --git a/packages/tools/package.json b/packages/tools/package.json index 81694a23..c7d40102 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -12,10 +12,17 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js", "default": "./dist/index.js" + }, + "./testing": { + "development": "./src/testing.ts", + "types": "./dist/testing.d.ts", + "import": "./dist/testing.js", + "default": "./dist/testing.js" } }, "dependencies": { "@clack/prompts": "1.0.0-alpha.9", + "memfs": "^4.64.0", "nano-spawn": "^1.0.2", "picocolors": "^1.1.1", "tslib": "^2.3.0" diff --git a/packages/tools/src/__tests__/harness-context-testing.test.ts b/packages/tools/src/__tests__/harness-context-testing.test.ts new file mode 100644 index 00000000..fd414e3b --- /dev/null +++ b/packages/tools/src/__tests__/harness-context-testing.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest'; +import { getFs, runWithHarnessContext } from '../harness-context.js'; +import { createInMemoryFileSystem } from '../testing.js'; + +describe('createInMemoryFileSystem', () => { + it('keeps files isolated from the host filesystem', () => { + const fs = createInMemoryFileSystem(); + + runWithHarnessContext({ fs }, () => { + getFs().mkdirSync('/project'); + getFs().writeFileSync('/project/config.json', '{"enabled":true}'); + + expect(getFs().readFileSync('/project/config.json', 'utf8')).toBe( + '{"enabled":true}' + ); + }); + }); +}); diff --git a/packages/tools/src/__tests__/harness-context.test.ts b/packages/tools/src/__tests__/harness-context.test.ts new file mode 100644 index 00000000..e276e6ec --- /dev/null +++ b/packages/tools/src/__tests__/harness-context.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it } from 'vitest'; +import { + getFs, + runWithHarnessContext, + type FileSystem, +} from '../harness-context.js'; + +const createFileSystem = (): FileSystem => + ({ + appendFileSync: () => undefined, + copyFileSync: () => undefined, + createWriteStream: () => undefined as never, + existsSync: () => false, + mkdirSync: () => undefined, + mkdtempSync: () => '/tmp/harness', + readFileSync: () => 'fake file', + readdirSync: () => [], + realpathSync: () => '/tmp/harness', + renameSync: () => undefined, + rmSync: () => undefined, + statSync: () => undefined as never, + unlinkSync: () => undefined, + utimesSync: () => undefined, + writeFileSync: () => undefined, + access: async () => undefined, + cp: async () => undefined, + mkdir: async () => undefined, + mkdtemp: async () => '/tmp/harness', + readFile: async () => Buffer.from('fake file'), + readdir: async () => [], + rename: async () => undefined, + rm: async () => undefined, + stat: async () => undefined as never, + writeFile: async () => undefined, + }) as unknown as FileSystem; + +describe('HarnessContext filesystem', () => { + it('uses the injected filesystem across async work', async () => { + const fs = createFileSystem(); + + await runWithHarnessContext({ fs }, async () => { + await Promise.resolve(); + + expect(getFs()).toBe(fs); + expect(getFs().readFileSync('/file', 'utf8')).toBe('fake file'); + }); + }); + + it('falls back to the real filesystem outside an injected context', () => { + expect(getFs().existsSync(__filename)).toBe(true); + }); +}); diff --git a/packages/tools/src/harness-context.ts b/packages/tools/src/harness-context.ts new file mode 100644 index 00000000..365126a4 --- /dev/null +++ b/packages/tools/src/harness-context.ts @@ -0,0 +1,80 @@ +import { AsyncLocalStorage } from 'node:async_hooks'; +import fs from 'node:fs'; +import * as fsPromises from 'node:fs/promises'; + +export type FileSystem = Pick< + typeof fs, + | 'appendFileSync' + | 'copyFileSync' + | 'createWriteStream' + | 'existsSync' + | 'mkdirSync' + | 'mkdtempSync' + | 'readFileSync' + | 'readdirSync' + | 'realpathSync' + | 'renameSync' + | 'rmSync' + | 'statSync' + | 'unlinkSync' + | 'utimesSync' + | 'writeFileSync' +> & + Pick< + typeof fsPromises, + | 'access' + | 'cp' + | 'mkdir' + | 'mkdtemp' + | 'readFile' + | 'readdir' + | 'rename' + | 'rm' + | 'stat' + | 'writeFile' + >; + +export type HarnessContext = { + fs: FileSystem; +}; + +const realFileSystem: FileSystem = { + appendFileSync: fs.appendFileSync, + copyFileSync: fs.copyFileSync, + createWriteStream: fs.createWriteStream, + existsSync: fs.existsSync, + mkdirSync: fs.mkdirSync, + mkdtempSync: fs.mkdtempSync, + readFileSync: fs.readFileSync, + readdirSync: fs.readdirSync, + realpathSync: fs.realpathSync, + renameSync: fs.renameSync, + rmSync: fs.rmSync, + statSync: fs.statSync, + unlinkSync: fs.unlinkSync, + utimesSync: fs.utimesSync, + writeFileSync: fs.writeFileSync, + access: fsPromises.access, + cp: fsPromises.cp, + mkdir: fsPromises.mkdir, + mkdtemp: fsPromises.mkdtemp, + readFile: fsPromises.readFile, + readdir: fsPromises.readdir, + rename: fsPromises.rename, + rm: fsPromises.rm, + stat: fsPromises.stat, + writeFile: fsPromises.writeFile, +}; + +const defaultHarnessContext: HarnessContext = { fs: realFileSystem }; +const storage = new AsyncLocalStorage(); + +export const runWithHarnessContext = ( + context: HarnessContext, + fn: () => T, +): T => storage.run(context, fn); + +export const getHarnessContext = (): HarnessContext => + storage.getStore() ?? defaultHarnessContext; + +export const getFs = (): FileSystem => getHarnessContext().fs; diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index a00382a6..bfc0823b 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -19,3 +19,4 @@ export * from './isInteractive.js'; export * from './diagnostics.js'; export * from './device-core-budget.js'; export * from './host-capabilities.js'; +export * from './harness-context.js'; diff --git a/packages/tools/src/testing.ts b/packages/tools/src/testing.ts new file mode 100644 index 00000000..826bfbbe --- /dev/null +++ b/packages/tools/src/testing.ts @@ -0,0 +1,34 @@ +import { createFsFromVolume, Volume } from 'memfs'; +import type { FileSystem } from './harness-context.js'; + +export const createInMemoryFileSystem = (): FileSystem => { + const fs = createFsFromVolume(new Volume()); + + return { + appendFileSync: fs.appendFileSync.bind(fs), + copyFileSync: fs.copyFileSync.bind(fs), + createWriteStream: fs.createWriteStream.bind(fs), + existsSync: fs.existsSync.bind(fs), + mkdirSync: fs.mkdirSync.bind(fs), + mkdtempSync: fs.mkdtempSync.bind(fs), + readFileSync: fs.readFileSync.bind(fs), + readdirSync: fs.readdirSync.bind(fs), + realpathSync: fs.realpathSync.bind(fs), + renameSync: fs.renameSync.bind(fs), + rmSync: fs.rmSync.bind(fs), + statSync: fs.statSync.bind(fs), + unlinkSync: fs.unlinkSync.bind(fs), + utimesSync: fs.utimesSync.bind(fs), + writeFileSync: fs.writeFileSync.bind(fs), + access: fs.promises.access.bind(fs.promises), + cp: fs.promises.cp.bind(fs.promises), + mkdir: fs.promises.mkdir.bind(fs.promises), + mkdtemp: fs.promises.mkdtemp.bind(fs.promises), + readFile: fs.promises.readFile.bind(fs.promises), + readdir: fs.promises.readdir.bind(fs.promises), + rename: fs.promises.rename.bind(fs.promises), + rm: fs.promises.rm.bind(fs.promises), + stat: fs.promises.stat.bind(fs.promises), + writeFile: fs.promises.writeFile.bind(fs.promises), + } as unknown as FileSystem; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55081f5a..9912625e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -597,6 +597,9 @@ importers: '@clack/prompts': specifier: 1.0.0-alpha.9 version: 1.0.0-alpha.9 + memfs: + specifier: ^4.64.0 + version: 4.68.2 nano-spawn: specifier: ^1.0.2 version: 1.0.2 @@ -1731,6 +1734,126 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jsonjoy.com/base64@1.1.2': + resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/base64@17.67.0': + resolution: {integrity: sha512-5SEsJGsm15aP8TQGkDfJvz9axgPwAEm98S5DxOuYe8e1EbfajcDmgeXXzccEjh+mLnjqEKrkBdjHWS5vFNwDdw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/buffers@1.2.1': + resolution: {integrity: sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/buffers@17.67.0': + resolution: {integrity: sha512-tfExRpYxBvi32vPs9ZHaTjSP4fHAfzSmcahOfNxtvGHcyJel+aibkPlGeBB+7AoC6hL7lXIE++8okecBxx7lcw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/codegen@1.0.0': + resolution: {integrity: sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/codegen@17.67.0': + resolution: {integrity: sha512-idnkUplROpdBOV0HMcwhsCUS5TRUi9poagdGs70A6S4ux9+/aPuKbh8+UYRTLYQHtXvAdNfQWXDqZEx5k4Dj2Q==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-core@4.68.2': + resolution: {integrity: sha512-PoBeUNEbjyLKKwCap2z8LkkqdhdfttS4rTHCALVuP65BdF+sAoyBqHo1m+uGTRBiQWv3H7MGfr8f7lY4pDwanw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-fsa@4.68.2': + resolution: {integrity: sha512-h6eGXlLGGMyPfNliDQrbuHKTB9Z29ksCLjreAmdBqrDOBdfrTrHssi+b9WLfrF+J2KzAbIt9OMjJu6AEpTnYkg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-node-builtins@4.68.2': + resolution: {integrity: sha512-V8WzQsW2YIrH3RxBGY6HZisxn+dDUltHgksVRuCdPEOXEkAfXuhL/01eHFrabNu84Dn13XuLqvcQUKOYVKAUOA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-node-to-fsa@4.68.2': + resolution: {integrity: sha512-4O1K4w5G4oJIpKpoa3WSLG83AsQYVnGv+aUSBGOvIUph9Axm6bB1mPlvldkNg2tBx/4dkiTlZnqNrK5SQ21Wtg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-node-utils@4.68.2': + resolution: {integrity: sha512-CxFwyG9fJr7dAKAd1uanNRNrRMtDDqbYJA8do53+M4LY7SxcBEEmMjC1zi9MOsBlVLHTXvA7OP9+n639UqtIcw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-node@4.68.2': + resolution: {integrity: sha512-Kpj519Qk4OXG4+mZ8vTf9BEflliJrPueIDEVcbx8tAOufMJJ8IxR0WwdbcEvJol2KQog3PJjtALXVclorE+xbQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-print@4.68.2': + resolution: {integrity: sha512-cBABQmZJXig6bahwNka3+yPNGUF1GeMWbR76ZM1N2ajgCd+S+twZigCoe9+0ueZmkhM9xd2a7688Gy/ch7T+2w==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/fs-snapshot@4.68.2': + resolution: {integrity: sha512-Aix7+NM38LzvewM0T3PICSgFdF5BVCtVgsjNsrlDPGc9hiMgJzReTDDl2/elgl0A9USMl3QP27x5WwxZSOtrfw==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pack@1.21.0': + resolution: {integrity: sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pack@17.67.0': + resolution: {integrity: sha512-t0ejURcGaZsn1ClbJ/3kFqSOjlryd92eQY465IYrezsXmPcfHPE/av4twRSxf6WE+TkZgLY+71vCZbiIiFKA/w==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pointer@1.0.2': + resolution: {integrity: sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pointer@17.67.0': + resolution: {integrity: sha512-+iqOFInH+QZGmSuaybBUNdh7yvNrXvqR+h3wjXm0N/3JK1EyyFAeGJvqnmQL61d1ARLlk/wJdFKSL+LHJ1eaUA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/util@1.9.0': + resolution: {integrity: sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/util@17.67.0': + resolution: {integrity: sha512-6+8xBaz1rLSohlGh68D1pdw3AwDi9xydm8QNlAFkvnavCJYSze+pxoW2VKP8p308jtlMRLs5NTHfPlZLd4w7ew==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} @@ -4892,6 +5015,12 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob-to-regex.js@1.2.0: + resolution: {integrity: sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} @@ -5108,6 +5237,10 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + hyperdyperid@1.2.0: + resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} + engines: {node: '>=10.18'} + hyphenate-style-name@1.1.0: resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==} @@ -5988,6 +6121,9 @@ packages: medium-zoom@1.1.0: resolution: {integrity: sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ==} + memfs@4.68.2: + resolution: {integrity: sha512-Un1ElEBoIdPI9kg0sm3LebVEuEViodfIvlaG8Z1MFXa7ZnR9vWuPKUo3dt/vuWY2ivc05l76B5QOjdgCzAzmGw==} + memoize-one@5.2.1: resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} @@ -7633,6 +7769,12 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} + thingies@2.6.1: + resolution: {integrity: sha512-cV/CMGTK3M4MlnJ/0At6ismOw/A0EEniDNScajjz/Br3c1sqE72YD01rGpPTKwd27wAxI5Pr+6+0w8yofzFRYw==} + engines: {node: '>=10.18'} + peerDependencies: + tslib: ^2 + throat@5.0.0: resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} @@ -7695,6 +7837,12 @@ packages: resolution: {integrity: sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==} engines: {node: '>=14'} + tree-dump@1.1.0: + resolution: {integrity: sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -9565,6 +9713,134 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/base64@17.67.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/buffers@1.2.1(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/buffers@17.67.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/codegen@1.0.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/codegen@17.67.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/fs-core@4.68.2(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-node-builtins': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.68.2(tslib@2.8.1) + thingies: 2.6.1(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-fsa@4.68.2(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-core': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.68.2(tslib@2.8.1) + thingies: 2.6.1(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-node-builtins@4.68.2(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/fs-node-to-fsa@4.68.2(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-fsa': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.68.2(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-node-utils@4.68.2(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-node-builtins': 4.68.2(tslib@2.8.1) + glob-to-regex.js: 1.2.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-node@4.68.2(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-core': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-print': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-snapshot': 4.68.2(tslib@2.8.1) + glob-to-regex.js: 1.2.0(tslib@2.8.1) + thingies: 2.6.1(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-print@4.68.2(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/fs-node-utils': 4.68.2(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/fs-snapshot@4.68.2(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/json-pack': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/json-pack@1.21.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/base64': 1.1.2(tslib@2.8.1) + '@jsonjoy.com/buffers': 1.2.1(tslib@2.8.1) + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/json-pointer': 1.0.2(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + hyperdyperid: 1.2.0 + thingies: 2.6.1(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/json-pack@17.67.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/base64': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/codegen': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/json-pointer': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) + hyperdyperid: 1.2.0 + thingies: 2.6.1(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/json-pointer@1.0.2(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/json-pointer@17.67.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/util': 17.67.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/util@1.9.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/buffers': 1.2.1(tslib@2.8.1) + '@jsonjoy.com/codegen': 1.0.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/util@17.67.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/buffers': 17.67.0(tslib@2.8.1) + '@jsonjoy.com/codegen': 17.67.0(tslib@2.8.1) + tslib: 2.8.1 + '@mdx-js/mdx@3.1.1': dependencies: '@types/estree': 1.0.8 @@ -13814,6 +14090,10 @@ snapshots: dependencies: is-glob: 4.0.3 + glob-to-regex.js@1.2.0(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + glob-to-regexp@0.4.1: {} glob@10.4.5: @@ -14176,6 +14456,8 @@ snapshots: human-signals@2.1.0: {} + hyperdyperid@1.2.0: {} + hyphenate-style-name@1.1.0: {} iconv-lite@0.4.24: @@ -15413,6 +15695,23 @@ snapshots: medium-zoom@1.1.0: {} + memfs@4.68.2: + dependencies: + '@jsonjoy.com/fs-core': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-fsa': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-node': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-builtins': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-to-fsa': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-node-utils': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-print': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/fs-snapshot': 4.68.2(tslib@2.8.1) + '@jsonjoy.com/json-pack': 1.21.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.9.0(tslib@2.8.1) + glob-to-regex.js: 1.2.0(tslib@2.8.1) + thingies: 2.6.1(tslib@2.8.1) + tree-dump: 1.1.0(tslib@2.8.1) + tslib: 2.8.1 + memoize-one@5.2.1: {} memoize-one@6.0.0: {} @@ -17504,6 +17803,10 @@ snapshots: glob: 10.5.0 minimatch: 9.0.5 + thingies@2.6.1(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + throat@5.0.0: {} tinybench@2.9.0: {} @@ -17550,6 +17853,10 @@ snapshots: dependencies: punycode: 2.3.1 + tree-dump@1.1.0(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + tree-kill@1.2.2: {} trim-lines@3.0.1: {} From be66d6f0accbc951295664eaf8cbb666f3608181 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Tue, 21 Jul 2026 12:32:25 +0200 Subject: [PATCH 2/2] feat(github-action): inject filesystem context --- .../version-plan-1784629893636.md | 5 +++ .../ci/__tests__/plan-metro-restore.test.ts | 25 ++++++++----- .../src/ci/__tests__/plan-metro-save.test.ts | 37 +++++++++++-------- .../src/ci/__tests__/snapshot-metro.test.ts | 27 ++++++++------ packages/cli/src/ci/load-config.ts | 4 +- packages/cli/src/ci/metro-cache-inputs.ts | 8 ++-- packages/cli/src/ci/plan-metro-restore.ts | 4 +- packages/cli/src/ci/plan-metro-save.ts | 4 +- packages/cli/src/ci/snapshot-metro.ts | 4 +- packages/tools/package.json | 6 +++ 10 files changed, 76 insertions(+), 48 deletions(-) create mode 100644 .nx/version-plans/version-plan-1784629893636.md diff --git a/.nx/version-plans/version-plan-1784629893636.md b/.nx/version-plans/version-plan-1784629893636.md new file mode 100644 index 00000000..2bc6b191 --- /dev/null +++ b/.nx/version-plans/version-plan-1784629893636.md @@ -0,0 +1,5 @@ +--- +__default__: minor +--- + +Add an injectable filesystem context and in-memory filesystem helper for hermetic Harness integrations and tests. diff --git a/packages/cli/src/ci/__tests__/plan-metro-restore.test.ts b/packages/cli/src/ci/__tests__/plan-metro-restore.test.ts index 5e9aaa6d..2f2a31c4 100644 --- a/packages/cli/src/ci/__tests__/plan-metro-restore.test.ts +++ b/packages/cli/src/ci/__tests__/plan-metro-restore.test.ts @@ -1,7 +1,11 @@ -import fs from 'node:fs'; -import os from 'node:os'; import path from 'node:path'; +import { + runWithHarnessContext, + type FileSystem, +} from '@react-native-harness/tools/harness-context'; +import { createInMemoryFileSystem } from '@react-native-harness/tools/testing'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { runPlanMetroRestore } from '../plan-metro-restore.js'; const mocks = vi.hoisted(() => ({ getConfig: vi.fn(), @@ -23,16 +27,18 @@ vi.mock('@react-native-harness/cache', () => ({ describe('plan-restore', () => { let projectRoot: string; let githubOutputPath: string; + let fileSystem: FileSystem; let planRestoreMock: ReturnType; let writeKeysFileMock: ReturnType; beforeEach(() => { - vi.resetModules(); vi.clearAllMocks(); - projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gha-plan-restore-')); + fileSystem = createInMemoryFileSystem(); + projectRoot = '/project'; githubOutputPath = path.join(projectRoot, 'github-output.txt'); - fs.writeFileSync(githubOutputPath, ''); + fileSystem.mkdirSync(projectRoot, { recursive: true }); + fileSystem.writeFileSync(githubOutputPath, ''); process.env.INPUT_PROJECTROOT = projectRoot; process.env.GITHUB_OUTPUT = githubOutputPath; @@ -69,7 +75,6 @@ describe('plan-restore', () => { }); afterEach(() => { - fs.rmSync(projectRoot, { recursive: true, force: true }); delete process.env.INPUT_PROJECTROOT; delete process.env.GITHUB_OUTPUT; delete process.env.RUNNER_OS; @@ -77,13 +82,13 @@ describe('plan-restore', () => { }); it('writes metroRestoreKey and metroRestorePrefixes (as JSON) to GITHUB_OUTPUT', async () => { - await (await import('../plan-metro-restore.js')).runPlanMetroRestore(); + await runWithHarnessContext({ fs: fileSystem }, runPlanMetroRestore); await vi.waitFor(() => { expect(writeKeysFileMock).toHaveBeenCalled(); }); - const output = fs.readFileSync(githubOutputPath, 'utf8'); + const output = fileSystem.readFileSync(githubOutputPath, 'utf8'); expect(output).toContain('metroRestoreKey=harness-metro-linux-abc\n'); expect(output).toContain( 'metroRestorePrefixes=["harness-metro-linux-abc-"]\n' @@ -94,7 +99,7 @@ describe('plan-restore', () => { }); it('calls planRestore with the os and staticInputs assembled from the Metro key recipe', async () => { - await (await import('../plan-metro-restore.js')).runPlanMetroRestore(); + await runWithHarnessContext({ fs: fileSystem }, runPlanMetroRestore); await vi.waitFor(() => { expect(planRestoreMock).toHaveBeenCalled(); @@ -111,7 +116,7 @@ describe('plan-restore', () => { it('fails loudly (process.exit(1)) when GITHUB_OUTPUT is not set', async () => { delete process.env.GITHUB_OUTPUT; - await (await import('../plan-metro-restore.js')).runPlanMetroRestore(); + await runWithHarnessContext({ fs: fileSystem }, runPlanMetroRestore); await vi.waitFor(() => { expect(process.exit).toHaveBeenCalledWith(1); diff --git a/packages/cli/src/ci/__tests__/plan-metro-save.test.ts b/packages/cli/src/ci/__tests__/plan-metro-save.test.ts index 9526debf..16d83613 100644 --- a/packages/cli/src/ci/__tests__/plan-metro-save.test.ts +++ b/packages/cli/src/ci/__tests__/plan-metro-save.test.ts @@ -1,7 +1,11 @@ -import fs from 'node:fs'; -import os from 'node:os'; import path from 'node:path'; +import { + runWithHarnessContext, + type FileSystem, +} from '@react-native-harness/tools/harness-context'; +import { createInMemoryFileSystem } from '@react-native-harness/tools/testing'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { runPlanMetroSave } from '../plan-metro-save.js'; const mocks = vi.hoisted(() => ({ getConfig: vi.fn(), @@ -21,16 +25,18 @@ vi.mock('@react-native-harness/cache', () => ({ describe('plan-save', () => { let projectRoot: string; let githubOutputPath: string; + let fileSystem: FileSystem; let planSaveMock: ReturnType; let writeKeysFileMock: ReturnType; beforeEach(() => { - vi.resetModules(); vi.clearAllMocks(); - projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gha-plan-save-')); + fileSystem = createInMemoryFileSystem(); + projectRoot = '/project'; githubOutputPath = path.join(projectRoot, 'github-output.txt'); - fs.writeFileSync(githubOutputPath, ''); + fileSystem.mkdirSync(projectRoot, { recursive: true }); + fileSystem.writeFileSync(githubOutputPath, ''); process.env.INPUT_PROJECTROOT = projectRoot; process.env.GITHUB_OUTPUT = githubOutputPath; @@ -69,7 +75,6 @@ describe('plan-save', () => { }); afterEach(() => { - fs.rmSync(projectRoot, { recursive: true, force: true }); delete process.env.INPUT_PROJECTROOT; delete process.env.GITHUB_OUTPUT; delete process.env.RUNNER_OS; @@ -80,13 +85,13 @@ describe('plan-save', () => { }); it('writes metroShouldSave and metroSaveKey to GITHUB_OUTPUT', async () => { - await (await import('../plan-metro-save.js')).runPlanMetroSave(); + await runWithHarnessContext({ fs: fileSystem }, runPlanMetroSave); await vi.waitFor(() => { expect(writeKeysFileMock).toHaveBeenCalled(); }); - const output = fs.readFileSync(githubOutputPath, 'utf8'); + const output = fileSystem.readFileSync(githubOutputPath, 'utf8'); expect(output).toContain('metroShouldSave=true\n'); expect(output).toContain( 'metroSaveKey=harness-metro-linux-abc-contenthash\n' @@ -94,7 +99,7 @@ describe('plan-save', () => { }); it('parses the metroSnapshot input and builds the SavePolicy from env/input', async () => { - await (await import('../plan-metro-save.js')).runPlanMetroSave(); + await runWithHarnessContext({ fs: fileSystem }, runPlanMetroSave); await vi.waitFor(() => { expect(planSaveMock).toHaveBeenCalled(); @@ -110,7 +115,7 @@ describe('plan-save', () => { it('treats an unparseable metroSnapshot input as an empty snapshot instead of throwing', async () => { process.env.INPUT_METRO_SNAPSHOT = 'not json'; - await (await import('../plan-metro-save.js')).runPlanMetroSave(); + await runWithHarnessContext({ fs: fileSystem }, runPlanMetroSave); await vi.waitFor(() => { expect(planSaveMock).toHaveBeenCalled(); @@ -127,7 +132,7 @@ describe('plan-save', () => { it('defaults an unrecognized cacheSavePolicy input to "default-branch"', async () => { process.env.INPUT_CACHESAVEPOLICY = 'bogus'; - await (await import('../plan-metro-save.js')).runPlanMetroSave(); + await runWithHarnessContext({ fs: fileSystem }, runPlanMetroSave); await vi.waitFor(() => { expect(planSaveMock).toHaveBeenCalled(); @@ -156,7 +161,7 @@ describe('plan-save', () => { }, }); - await (await import('../plan-metro-save.js')).runPlanMetroSave(); + await runWithHarnessContext({ fs: fileSystem }, runPlanMetroSave); await vi.waitFor(() => { expect(writeKeysFileMock).toHaveBeenCalled(); @@ -179,7 +184,7 @@ describe('plan-save', () => { }, }); - await (await import('../plan-metro-save.js')).runPlanMetroSave(); + await runWithHarnessContext({ fs: fileSystem }, runPlanMetroSave); await vi.waitFor(() => { expect(writeKeysFileMock).toHaveBeenCalled(); @@ -203,7 +208,7 @@ describe('plan-save', () => { }, }); - await (await import('../plan-metro-save.js')).runPlanMetroSave(); + await runWithHarnessContext({ fs: fileSystem }, runPlanMetroSave); await vi.waitFor(() => { expect(writeKeysFileMock).toHaveBeenCalled(); @@ -227,7 +232,7 @@ describe('plan-save', () => { }, }); - await (await import('../plan-metro-save.js')).runPlanMetroSave(); + await runWithHarnessContext({ fs: fileSystem }, runPlanMetroSave); await vi.waitFor(() => { expect(writeKeysFileMock).toHaveBeenCalled(); @@ -241,7 +246,7 @@ describe('plan-save', () => { it('fails loudly (process.exit(1)) when GITHUB_OUTPUT is not set', async () => { delete process.env.GITHUB_OUTPUT; - await (await import('../plan-metro-save.js')).runPlanMetroSave(); + await runWithHarnessContext({ fs: fileSystem }, runPlanMetroSave); await vi.waitFor(() => { expect(process.exit).toHaveBeenCalledWith(1); diff --git a/packages/cli/src/ci/__tests__/snapshot-metro.test.ts b/packages/cli/src/ci/__tests__/snapshot-metro.test.ts index 0eb44547..0795453f 100644 --- a/packages/cli/src/ci/__tests__/snapshot-metro.test.ts +++ b/packages/cli/src/ci/__tests__/snapshot-metro.test.ts @@ -1,7 +1,11 @@ -import fs from 'node:fs'; -import os from 'node:os'; import path from 'node:path'; +import { + runWithHarnessContext, + type FileSystem, +} from '@react-native-harness/tools/harness-context'; +import { createInMemoryFileSystem } from '@react-native-harness/tools/testing'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { runSnapshotMetro } from '../snapshot-metro.js'; const mocks = vi.hoisted(() => ({ getConfig: vi.fn(), @@ -19,15 +23,17 @@ vi.mock('@react-native-harness/cache', () => ({ describe('snapshot-metro', () => { let projectRoot: string; let githubOutputPath: string; + let fileSystem: FileSystem; let snapshotMock: ReturnType; beforeEach(() => { - vi.resetModules(); vi.clearAllMocks(); - projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gha-snapshot-metro-')); + fileSystem = createInMemoryFileSystem(); + projectRoot = '/project'; githubOutputPath = path.join(projectRoot, 'github-output.txt'); - fs.writeFileSync(githubOutputPath, ''); + fileSystem.mkdirSync(projectRoot, { recursive: true }); + fileSystem.writeFileSync(githubOutputPath, ''); process.env.INPUT_PROJECTROOT = projectRoot; process.env.GITHUB_OUTPUT = githubOutputPath; @@ -51,7 +57,6 @@ describe('snapshot-metro', () => { }); afterEach(() => { - fs.rmSync(projectRoot, { recursive: true, force: true }); delete process.env.INPUT_PROJECTROOT; delete process.env.GITHUB_OUTPUT; delete process.env.METRO_RESTORED_KEY; @@ -59,13 +64,13 @@ describe('snapshot-metro', () => { }); it('writes the current cache snapshot to GITHUB_OUTPUT as metroSnapshot', async () => { - await (await import('../snapshot-metro.js')).runSnapshotMetro(); + await runWithHarnessContext({ fs: fileSystem }, runSnapshotMetro); await vi.waitFor(() => { expect(snapshotMock).toHaveBeenCalled(); }); - const output = fs.readFileSync(githubOutputPath, 'utf8'); + const output = fileSystem.readFileSync(githubOutputPath, 'utf8'); expect(output).toContain( 'metroSnapshot={"metro":[{"path":"metro/a","sizeBytes":3,"mtimeMs":1}]}\n' ); @@ -80,7 +85,7 @@ describe('snapshot-metro', () => { ], }); - await (await import('../snapshot-metro.js')).runSnapshotMetro(); + await runWithHarnessContext({ fs: fileSystem }, runSnapshotMetro); await vi.waitFor(() => { expect(snapshotMock).toHaveBeenCalled(); @@ -94,7 +99,7 @@ describe('snapshot-metro', () => { it('reports a cold start when no cache entry matched', async () => { delete process.env.METRO_RESTORED_KEY; - await (await import('../snapshot-metro.js')).runSnapshotMetro(); + await runWithHarnessContext({ fs: fileSystem }, runSnapshotMetro); await vi.waitFor(() => { expect(snapshotMock).toHaveBeenCalled(); @@ -108,7 +113,7 @@ describe('snapshot-metro', () => { it('fails loudly (process.exit(1)) when GITHUB_OUTPUT is not set', async () => { delete process.env.GITHUB_OUTPUT; - await (await import('../snapshot-metro.js')).runSnapshotMetro(); + await runWithHarnessContext({ fs: fileSystem }, runSnapshotMetro); await vi.waitFor(() => { expect(process.exit).toHaveBeenCalledWith(1); diff --git a/packages/cli/src/ci/load-config.ts b/packages/cli/src/ci/load-config.ts index 3bd2ece1..6db3a769 100644 --- a/packages/cli/src/ci/load-config.ts +++ b/packages/cli/src/ci/load-config.ts @@ -1,5 +1,5 @@ import { getConfig } from '@react-native-harness/config'; -import fs from 'node:fs'; +import { getFs } from '@react-native-harness/tools/harness-context'; import { createRequire } from 'node:module'; import { relativeToWorkspaceRoot, resolveProjectRoot } from './workspace-root.js'; @@ -144,7 +144,7 @@ export const runLoadConfig = async (): Promise => { const output = `config=${JSON.stringify( resolvedRunner )}\nprojectRoot=${relativeProjectRoot}\n`; - fs.appendFileSync(githubOutput, output); + getFs().appendFileSync(githubOutput, output); } catch (error) { if (error instanceof Error) { console.error(error.message); diff --git a/packages/cli/src/ci/metro-cache-inputs.ts b/packages/cli/src/ci/metro-cache-inputs.ts index 46733bf7..5906de70 100644 --- a/packages/cli/src/ci/metro-cache-inputs.ts +++ b/packages/cli/src/ci/metro-cache-inputs.ts @@ -1,7 +1,7 @@ -import fs from 'node:fs'; import path from 'node:path'; import { createRequire } from 'node:module'; import { computeMetroStaticInputs } from '@react-native-harness/cache'; +import { getFs } from '@react-native-harness/tools/harness-context'; const require = createRequire(import.meta.url); @@ -19,7 +19,7 @@ export const resolveRepoRoot = (projectRoot: string): string => { let dir = projectRoot; while (true) { - if (fs.existsSync(path.join(dir, '.git'))) { + if (getFs().existsSync(path.join(dir, '.git'))) { return dir; } @@ -68,7 +68,9 @@ const resolveBundlerMetroPackageJson = (projectRoot: string): string => { export const resolveBundlerMetroVersion = (projectRoot: string): string => { try { const packageJsonPath = resolveBundlerMetroPackageJson(projectRoot); - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + const packageJson = JSON.parse( + getFs().readFileSync(packageJsonPath, 'utf8') + ); if (typeof packageJson.version !== 'string') { throw new Error('"version" field is missing or not a string'); diff --git a/packages/cli/src/ci/plan-metro-restore.ts b/packages/cli/src/ci/plan-metro-restore.ts index aa03b78d..20437c31 100644 --- a/packages/cli/src/ci/plan-metro-restore.ts +++ b/packages/cli/src/ci/plan-metro-restore.ts @@ -1,6 +1,6 @@ -import fs from 'node:fs'; import { createHarnessCache } from '@react-native-harness/cache'; import { getConfig } from '@react-native-harness/config'; +import { getFs } from '@react-native-harness/tools/harness-context'; import { resolveMetroStaticInputs } from './metro-cache-inputs.js'; import { resolveProjectRoot } from './workspace-root.js'; @@ -52,7 +52,7 @@ export const runPlanMetroRestore = async (): Promise => { metroPlan?.restorePrefixes ?? [] )}\n`; - fs.appendFileSync(githubOutput, output); + getFs().appendFileSync(githubOutput, output); } catch (error) { if (error instanceof Error) { console.error(error.message); diff --git a/packages/cli/src/ci/plan-metro-save.ts b/packages/cli/src/ci/plan-metro-save.ts index f509057b..959420f3 100644 --- a/packages/cli/src/ci/plan-metro-save.ts +++ b/packages/cli/src/ci/plan-metro-save.ts @@ -1,4 +1,3 @@ -import fs from 'node:fs'; import { createHarnessCache, type CacheSnapshot, @@ -6,6 +5,7 @@ import { type SavePolicy, } from '@react-native-harness/cache'; import { getConfig } from '@react-native-harness/config'; +import { getFs } from '@react-native-harness/tools/harness-context'; import { formatMegabytes } from './format-bytes.js'; import { resolveMetroStaticInputs } from './metro-cache-inputs.js'; import { resolveProjectRoot } from './workspace-root.js'; @@ -120,7 +120,7 @@ export const runPlanMetroSave = async (): Promise => { `metroShouldSave=${metroPlan?.shouldSave ? 'true' : 'false'}\n` + `metroSaveKey=${metroPlan?.saveKey ?? ''}\n`; - fs.appendFileSync(githubOutput, output); + getFs().appendFileSync(githubOutput, output); } catch (error) { if (error instanceof Error) { console.error(error.message); diff --git a/packages/cli/src/ci/snapshot-metro.ts b/packages/cli/src/ci/snapshot-metro.ts index 1f9a721c..b24379fa 100644 --- a/packages/cli/src/ci/snapshot-metro.ts +++ b/packages/cli/src/ci/snapshot-metro.ts @@ -1,6 +1,6 @@ -import fs from 'node:fs'; import { createHarnessCache } from '@react-native-harness/cache'; import { getConfig } from '@react-native-harness/config'; +import { getFs } from '@react-native-harness/tools/harness-context'; import { formatMegabytes } from './format-bytes.js'; import { resolveProjectRoot } from './workspace-root.js'; @@ -52,7 +52,7 @@ export const runSnapshotMetro = async (): Promise => { const output = `metroSnapshot=${JSON.stringify(snapshotAfterRestore)}\n`; - fs.appendFileSync(githubOutput, output); + getFs().appendFileSync(githubOutput, output); } catch (error) { if (error instanceof Error) { console.error(error.message); diff --git a/packages/tools/package.json b/packages/tools/package.json index c7d40102..79c797a0 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -13,6 +13,12 @@ "import": "./dist/index.js", "default": "./dist/index.js" }, + "./harness-context": { + "development": "./src/harness-context.ts", + "types": "./dist/harness-context.d.ts", + "import": "./dist/harness-context.js", + "default": "./dist/harness-context.js" + }, "./testing": { "development": "./src/testing.ts", "types": "./dist/testing.d.ts",