fix: import in extensions (#3276)
Maintain Release Merge PR / update-release-pr (push) Has been cancelled
Notify HeyPuter / notify (push) Has been cancelled
release-please / release-please (push) Has been cancelled

This commit is contained in:
Daniel Salazar
2026-06-18 21:30:00 -07:00
committed by GitHub
parent c8625b3396
commit d3cb89d8c9
4 changed files with 41 additions and 37 deletions
-12
View File
@@ -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",
@@ -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<string, RequestHandler | null>;
const additionalRoutePaths: Record<string, string> = {};
@@ -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'
+7 -1
View File
@@ -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');
});
});
+34 -5
View File
@@ -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 = <T>(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