diff --git a/package-lock.json b/package-lock.json index 1c4d5b2b6..85dea9746 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1514,18 +1514,6 @@ "sisteransi": "^1.0.5" } }, - "node_modules/@clack/prompts/node_modules/is-unicode-supported": { - "version": "1.3.0", - "extraneous": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@cloudflare/kv-asset-handler": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz", diff --git a/src/backend/controllers/fs/LegacyFSController.ts b/src/backend/controllers/fs/LegacyFSController.ts index 674a63916..34c971c79 100644 --- a/src/backend/controllers/fs/LegacyFSController.ts +++ b/src/backend/controllers/fs/LegacyFSController.ts @@ -46,14 +46,6 @@ import { } from './legacyFsHelpers.js'; import { RouteOptions } from '../../core/http/index.js'; -/** - * Legacy FS routes, implemented as thin shims over `FSService`. - * - * Each shim parses the request shape (FSNodeParam-style `{ path, uid, id }` - * or `{ parent, name }`), invokes the service method, and returns the - * snake_case response clients expect. - */ - type RouterCache = Map; const additionalRoutePaths: Record = {}; @@ -1631,17 +1623,6 @@ export class LegacyFSController extends PuterController { download.body.pipe(res); }; - // Helpers for writeFile - // -- GUI event emission ------------------------------------------- - // - // Fire-and-forget `outer.gui.item.*` events so SocketService, - // BroadcastService, WorkerDriver (hot-reload), and cache-invalidation - // listeners pick up mutations made through the legacy (bare-path) routes. - // FSController (v2-native /fs/* routes) emits these from its own handlers; - // LegacyFSController delegates to the same FSService but needs its - // own emissions because the service layer deliberately doesn't emit GUI - // events (that's a controller concern). - async #emitGuiEvent( eventName: | 'outer.gui.item.added' diff --git a/src/backend/extensions.test.ts b/src/backend/extensions.test.ts index 009a876b0..57a3fdf5e 100644 --- a/src/backend/extensions.test.ts +++ b/src/backend/extensions.test.ts @@ -78,6 +78,12 @@ describe('extension.import("client") optional client access', () => { return null; } }; - expect(probe()).toBe(fake); + // The import proxy method-binds, so the result is a binding proxy over + // `fake` rather than the raw reference (identity is intentionally not + // preserved). What the probe pattern locks is that a registered client + // surfaces a callable method. + const result = probe(); + expect(result).not.toBeNull(); + expect(typeof (result as { query: unknown }).query).toBe('function'); }); }); diff --git a/src/backend/extensions.ts b/src/backend/extensions.ts index e66e878f1..87420f9a1 100644 --- a/src/backend/extensions.ts +++ b/src/backend/extensions.ts @@ -201,6 +201,35 @@ const makeUseFn = (): ExtensionUseFn => { * `'store:baz'` / `'controller:qux'` / `'driver:fred'` — returns a lazy * proxy to the registered instance (thrown on use-before-init). */ +/** + * Wrap a resolved layer instance so that pulling a method off the import + * comes out *bound* to the instance. Extensions routinely grab a method as a + * bare reference — `const { write } = extension.import('service').fs` or + * `const w = svc.fs.write` — then call it detached; without binding, `this` + * is `undefined` and the method's private-field access throws on the first + * line. Getters keep the real instance as their receiver, so private-field + * reads inside accessors still resolve. Only `get` is trapped; writes, `in`, + * and descriptor reads fall through to the instance unchanged. + * + * Trade-off: each method access returns a fresh bound function, so reference + * identity is not stable (`svc.fs.write !== svc.fs.write`). That's acceptable + * for the import surface, where instances are grabbed once and methods called. + */ +const bindLayerMethods = (instance: T): T => { + if (instance === null || typeof instance !== 'object') { + return instance; + } + return new Proxy(instance as object, { + get(target, prop) { + const value = Reflect.get(target, prop, target); + return typeof value === 'function' + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + (value as (...a: any[]) => unknown).bind(target) + : value; + }, + }) as T; +}; + export const extension = { // -- Config access ----------------------------------------------- // @@ -324,7 +353,7 @@ export const extension = { }; return new Proxy({}, proxyProxyHandler) as object; } - return proxiedObj; + return bindLayerMethods(proxiedObj); }, }; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -352,7 +381,7 @@ export const extension = { }; return new Proxy({}, proxyProxyHandler) as object; } - return proxiedObj; + return bindLayerMethods(proxiedObj); }, }; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -380,7 +409,7 @@ export const extension = { }; return new Proxy({}, proxyProxyHandler) as object; } - return proxiedObj; + return bindLayerMethods(proxiedObj); }, }; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -408,7 +437,7 @@ export const extension = { }; return new Proxy({}, proxyProxyHandler) as object; } - return proxiedObj; + return bindLayerMethods(proxiedObj); }, }; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -436,7 +465,7 @@ export const extension = { }; return new Proxy({}, proxyProxyHandler) as object; } - return proxiedObj; + return bindLayerMethods(proxiedObj); }, }; // eslint-disable-next-line @typescript-eslint/no-explicit-any