@portel/photon-core 2.25.0 → 2.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -0
- package/dist/asset-discovery.d.ts +8 -2
- package/dist/asset-discovery.d.ts.map +1 -1
- package/dist/asset-discovery.js +46 -9
- package/dist/asset-discovery.js.map +1 -1
- package/dist/base.d.ts +76 -0
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +80 -0
- package/dist/base.js.map +1 -1
- package/dist/cf.d.ts +121 -0
- package/dist/cf.d.ts.map +1 -0
- package/dist/cf.js +65 -0
- package/dist/cf.js.map +1 -0
- package/dist/cloudflare.d.ts +110 -0
- package/dist/cloudflare.d.ts.map +1 -0
- package/dist/cloudflare.js +138 -0
- package/dist/cloudflare.js.map +1 -0
- package/dist/collections/Collection.d.ts +12 -0
- package/dist/collections/Collection.d.ts.map +1 -1
- package/dist/collections/Collection.js +19 -0
- package/dist/collections/Collection.js.map +1 -1
- package/dist/env.d.ts +29 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +2 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mixins.d.ts.map +1 -1
- package/dist/mixins.js +10 -0
- package/dist/mixins.js.map +1 -1
- package/dist/photon-loader-lite.d.ts +17 -0
- package/dist/photon-loader-lite.d.ts.map +1 -1
- package/dist/photon-loader-lite.js +150 -1
- package/dist/photon-loader-lite.js.map +1 -1
- package/dist/schema-extractor.d.ts +25 -6
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +86 -7
- package/dist/schema-extractor.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +3 -2
- package/src/asset-discovery.ts +44 -9
- package/src/base.ts +86 -0
- package/src/cf.ts +165 -0
- package/src/cloudflare.ts +198 -0
- package/src/collections/Collection.ts +19 -0
- package/src/env.ts +28 -0
- package/src/index.ts +27 -0
- package/src/mixins.ts +12 -0
- package/src/photon-loader-lite.ts +185 -0
- package/src/schema-extractor.ts +119 -9
- package/src/types.ts +16 -1
package/src/env.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CloudflareEnv — raw Cloudflare Worker `env`, surfaced as an explicit
|
|
3
|
+
* constructor injection so the import line itself signals that the
|
|
4
|
+
* importing photon depends on a Cloudflare deploy target.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import type { CloudflareEnv } from "@portel/photon";
|
|
9
|
+
*
|
|
10
|
+
* interface MyBindings {
|
|
11
|
+
* WEATHER_KV: KVNamespace;
|
|
12
|
+
* PHOTOS: R2Bucket;
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* export class Weather {
|
|
16
|
+
* constructor(private env: CloudflareEnv<MyBindings>) {}
|
|
17
|
+
* async forecast() {
|
|
18
|
+
* await this.env.WEATHER_KV.put("k", "v");
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* The default loose type (`Record<string, unknown>`) keeps the no-types
|
|
24
|
+
* path working. Authors who want strict binding shapes pass their own
|
|
25
|
+
* type parameter; future codegen from `protected cfBindings` can fill
|
|
26
|
+
* this in automatically.
|
|
27
|
+
*/
|
|
28
|
+
export type CloudflareEnv<T = Record<string, unknown>> = T;
|
package/src/index.ts
CHANGED
|
@@ -154,6 +154,33 @@ export {
|
|
|
154
154
|
// Core base class with lifecycle hooks
|
|
155
155
|
export { Photon, Photon as PhotonMCP } from './base.js';
|
|
156
156
|
|
|
157
|
+
// Cloudflare deploy-target injections (constructor-injected, not on Photon)
|
|
158
|
+
export {
|
|
159
|
+
type Cloudflare,
|
|
160
|
+
type ScopedBindingCategory,
|
|
161
|
+
bindingNameFor,
|
|
162
|
+
notConfiguredCloudflare,
|
|
163
|
+
createCloudflareFromEnv,
|
|
164
|
+
SHARED_AI_BINDING,
|
|
165
|
+
SHARED_IMAGES_BINDING,
|
|
166
|
+
SHARED_BROWSER_BINDING,
|
|
167
|
+
} from './cloudflare.js';
|
|
168
|
+
export { type CloudflareEnv } from './env.js';
|
|
169
|
+
// Structural CF binding types — published so hosts can type their own
|
|
170
|
+
// CF adapters against the same shape `Cloudflare` exposes (e.g. unit
|
|
171
|
+
// tests that mock `cf.kv()` returns).
|
|
172
|
+
export {
|
|
173
|
+
type R2BucketLike,
|
|
174
|
+
type KVNamespaceLike,
|
|
175
|
+
type D1DatabaseLike,
|
|
176
|
+
type D1PreparedStatementLike,
|
|
177
|
+
type QueueLike,
|
|
178
|
+
type VectorizeIndexLike,
|
|
179
|
+
type AiLike,
|
|
180
|
+
type ImagesBindingLike,
|
|
181
|
+
type FetcherLike,
|
|
182
|
+
} from './cf.js';
|
|
183
|
+
|
|
157
184
|
// Mixin for capability injection without requiring inheritance
|
|
158
185
|
export { withPhotonCapabilities } from './mixins.js';
|
|
159
186
|
|
package/src/mixins.ts
CHANGED
|
@@ -84,6 +84,18 @@ export function withPhotonCapabilities<T extends Constructor>(Base: T): T {
|
|
|
84
84
|
*/
|
|
85
85
|
private _schedule?: ScheduleProvider;
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Cloudflare Worker env when deployed; undefined on local hosts.
|
|
89
|
+
* See Photon.env for the full contract.
|
|
90
|
+
*/
|
|
91
|
+
readonly env?: Record<string, unknown>;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* True when the active /mcp request passed the PHOTON_MCP_BEARER
|
|
95
|
+
* gate. See Photon.mcpAuthed for the full contract.
|
|
96
|
+
*/
|
|
97
|
+
readonly mcpAuthed?: boolean;
|
|
98
|
+
|
|
87
99
|
/**
|
|
88
100
|
* Cross-photon call handler - injected by runtime
|
|
89
101
|
* @internal
|
|
@@ -38,6 +38,12 @@ import { withPhotonCapabilities } from './mixins.js';
|
|
|
38
38
|
import { MemoryProvider } from './memory.js';
|
|
39
39
|
import { ScheduleProvider } from './schedule.js';
|
|
40
40
|
import { toEnvVarName, parseEnvValue, type MissingParamInfo } from './env-utils.js';
|
|
41
|
+
import { Photon as PhotonBase } from './base.js';
|
|
42
|
+
import {
|
|
43
|
+
type Cloudflare,
|
|
44
|
+
notConfiguredCloudflare,
|
|
45
|
+
createCloudflareFromEnv,
|
|
46
|
+
} from './cloudflare.js';
|
|
41
47
|
import type { ExtractedSchema } from './types.js';
|
|
42
48
|
import type { MCPClientFactory } from '@portel/mcp';
|
|
43
49
|
import { getCacheDir } from './data-paths.js';
|
|
@@ -59,6 +65,22 @@ export interface PhotonOptions {
|
|
|
59
65
|
sessionId?: string;
|
|
60
66
|
/** Namespace for data path resolution (marketplace owner). Defaults to 'local'. */
|
|
61
67
|
namespace?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Build a `Cloudflare` runtime for a given photon. Hosts that bring CF
|
|
70
|
+
* support (local miniflare, deployed Worker) supply a factory; the
|
|
71
|
+
* loader calls it whenever a photon's constructor needs `Cloudflare`
|
|
72
|
+
* (or whenever `this.cf.*` is detected on a plain class). When the
|
|
73
|
+
* factory is absent the loader injects a throwing-Proxy fallback so
|
|
74
|
+
* the diagnostic remains clear.
|
|
75
|
+
*/
|
|
76
|
+
cloudflareFactory?: (photonName: string) => Cloudflare;
|
|
77
|
+
/**
|
|
78
|
+
* Raw Cloudflare Worker `env`. Wired through to constructor params
|
|
79
|
+
* typed `CloudflareEnv` / `CloudflareEnv<T>`. Only present when the
|
|
80
|
+
* loader is being driven by the deployed Worker template (or by a
|
|
81
|
+
* test that wants to simulate it).
|
|
82
|
+
*/
|
|
83
|
+
cloudflareEnv?: Record<string, unknown>;
|
|
62
84
|
}
|
|
63
85
|
|
|
64
86
|
export interface PhotonEvent {
|
|
@@ -263,6 +285,24 @@ async function loadPhotonInternal(
|
|
|
263
285
|
// 9. Instantiate
|
|
264
286
|
const instance = new EnhancedClass(...constructorArgs) as Record<string, any>;
|
|
265
287
|
|
|
288
|
+
// 9a. Forgiving auto-inject: classes that reference `this.cf.*` or
|
|
289
|
+
// `this.cfEnv.*` without declaring the matching constructor param
|
|
290
|
+
// still get the field populated, so authoring stays loose. The
|
|
291
|
+
// explicit-injection path (a typed constructor param) takes
|
|
292
|
+
// precedence; this only fills gaps. Mirrors how the classic loader
|
|
293
|
+
// injects `_callHandler` etc. for plain classes.
|
|
294
|
+
const detected = detectCapabilities(source);
|
|
295
|
+
const hasExplicitCloudflare = injections.some(i => i.injectionType === 'cloudflare');
|
|
296
|
+
const hasExplicitCloudflareEnv = injections.some(i => i.injectionType === 'cloudflareEnv');
|
|
297
|
+
if (detected.has('cloudflare') && !hasExplicitCloudflare && instance.cf === undefined) {
|
|
298
|
+
instance.cf = options.cloudflareFactory
|
|
299
|
+
? options.cloudflareFactory(photonName)
|
|
300
|
+
: notConfiguredCloudflare();
|
|
301
|
+
}
|
|
302
|
+
if (detected.has('cloudflareEnv') && !hasExplicitCloudflareEnv && instance.cfEnv === undefined) {
|
|
303
|
+
instance.cfEnv = options.cloudflareEnv ?? makeThrowingCloudflareEnv();
|
|
304
|
+
}
|
|
305
|
+
|
|
266
306
|
// 10. Set photon identity. Namespace mirrors the file's position under
|
|
267
307
|
// baseDir (same rule the classic loader applies). Falls back to 'local'
|
|
268
308
|
// when no baseDir context is available. See
|
|
@@ -450,6 +490,38 @@ async function resolveConstructorArgs(
|
|
|
450
490
|
break;
|
|
451
491
|
}
|
|
452
492
|
|
|
493
|
+
case 'photonRuntime': {
|
|
494
|
+
// `private photon: Photon` — inject a Photon instance configured
|
|
495
|
+
// identically to the host so `this.photon.memory.set(...)`,
|
|
496
|
+
// `this.photon.emit(...)`, etc. resolve to the same scope as
|
|
497
|
+
// the equivalent `extends Photon` calls would. Identity-stable
|
|
498
|
+
// per host load.
|
|
499
|
+
const runtime = createPhotonRuntimeForHost(photonName, currentPath, options);
|
|
500
|
+
values.push(runtime);
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
case 'cloudflare': {
|
|
505
|
+
// `private cf: Cloudflare` — wrapped, auto-named CF surface.
|
|
506
|
+
const cf = options.cloudflareFactory
|
|
507
|
+
? options.cloudflareFactory(photonName)
|
|
508
|
+
: notConfiguredCloudflare();
|
|
509
|
+
values.push(cf);
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
case 'cloudflareEnv': {
|
|
514
|
+
// `private cfEnv: CloudflareEnv` — raw Worker env. On hosts
|
|
515
|
+
// without a CF runtime, hand back a throwing Proxy so the
|
|
516
|
+
// diagnostic names the imported symbol clearly.
|
|
517
|
+
if (options.cloudflareEnv) {
|
|
518
|
+
values.push(options.cloudflareEnv);
|
|
519
|
+
} else {
|
|
520
|
+
values.push(makeThrowingCloudflareEnv());
|
|
521
|
+
}
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
|
|
453
525
|
default:
|
|
454
526
|
values.push(undefined);
|
|
455
527
|
}
|
|
@@ -466,6 +538,119 @@ async function resolveConstructorArgs(
|
|
|
466
538
|
return values;
|
|
467
539
|
}
|
|
468
540
|
|
|
541
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
542
|
+
// Photon-as-injection helpers
|
|
543
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Build the `Photon` instance handed to `private photon: Photon` ctor
|
|
547
|
+
* params. We instantiate the real base class and configure it with the
|
|
548
|
+
* host photon's identity so every capability (`memory`, `schedule`,
|
|
549
|
+
* `emit`, `call`, `mcp`, `caller`, `confirm`, `elicit`, `sample`,
|
|
550
|
+
* `photon.use`, ...) resolves to the same scope an equivalent
|
|
551
|
+
* `extends Photon` consumer would see. The result is shape-equivalent
|
|
552
|
+
* to a normal Photon, so authors can swap between `extends Photon` and
|
|
553
|
+
* constructor injection without touching any call site beyond the
|
|
554
|
+
* access path (`this.memory` vs `this.photon.memory`).
|
|
555
|
+
*
|
|
556
|
+
* The `_callHandler` and `setMCPFactory` wiring is deferred to the
|
|
557
|
+
* caller (see step 14 of `loadPhotonInternal`), which assigns the
|
|
558
|
+
* resolver after constructor args are built. Constructor-time access
|
|
559
|
+
* to `photon.memory` works because MemoryProvider is lazy-resolved on
|
|
560
|
+
* first use and the photon name / baseDir are set up here directly.
|
|
561
|
+
*/
|
|
562
|
+
function createPhotonRuntimeForHost(
|
|
563
|
+
photonName: string,
|
|
564
|
+
currentPath: string,
|
|
565
|
+
options: PhotonOptions,
|
|
566
|
+
): InstanceType<typeof PhotonBase> {
|
|
567
|
+
const runtime = new PhotonBase() as PhotonBase & Record<string, unknown>;
|
|
568
|
+
runtime._photonName = photonName;
|
|
569
|
+
runtime._photonNamespace =
|
|
570
|
+
options.namespace ?? deriveNamespace(currentPath, options.baseDir);
|
|
571
|
+
runtime._baseDir = options.baseDir;
|
|
572
|
+
runtime._photonFilePath = currentPath;
|
|
573
|
+
if (options.sessionId) {
|
|
574
|
+
runtime._sessionId = options.sessionId;
|
|
575
|
+
}
|
|
576
|
+
const setMCPFactory = (runtime as { setMCPFactory?: (f: MCPClientFactory) => void }).setMCPFactory;
|
|
577
|
+
if (options.mcpFactory && typeof setMCPFactory === 'function') {
|
|
578
|
+
setMCPFactory.call(runtime, options.mcpFactory);
|
|
579
|
+
}
|
|
580
|
+
// Cross-photon calls flow through the same in-process resolver the
|
|
581
|
+
// host instance uses. `loadPhotonInternal` re-assigns `_callHandler`
|
|
582
|
+
// on the host instance below; the runtime facade gets its own copy
|
|
583
|
+
// pointed at the same target resolver so injected callers and
|
|
584
|
+
// extends-Photon callers see identical behaviour.
|
|
585
|
+
runtime._callHandler = async (
|
|
586
|
+
targetPhotonName: string,
|
|
587
|
+
method: string,
|
|
588
|
+
params: Record<string, unknown>,
|
|
589
|
+
) => {
|
|
590
|
+
const targetPath = resolvePhotonPath(targetPhotonName, currentPath, options.baseDir);
|
|
591
|
+
const target = await photon(targetPath, {
|
|
592
|
+
baseDir: options.baseDir,
|
|
593
|
+
mcpFactory: options.mcpFactory,
|
|
594
|
+
sessionId: options.sessionId,
|
|
595
|
+
});
|
|
596
|
+
return (target as Record<string, (p: Record<string, unknown>) => Promise<unknown>>)[method](params);
|
|
597
|
+
};
|
|
598
|
+
// `this.photon.photon.use()` parity: `extends Photon` instances get
|
|
599
|
+
// `_photonResolver` set by the host loader; the injected runtime
|
|
600
|
+
// needs the same resolver so dynamic photon access works identically
|
|
601
|
+
// through both consumption modes.
|
|
602
|
+
runtime._photonResolver = async (name: string, instanceName?: string) => {
|
|
603
|
+
const targetPath = resolvePhotonPath(name, currentPath, options.baseDir);
|
|
604
|
+
return photon(targetPath, {
|
|
605
|
+
baseDir: options.baseDir,
|
|
606
|
+
mcpFactory: options.mcpFactory,
|
|
607
|
+
sessionId: options.sessionId,
|
|
608
|
+
instanceName,
|
|
609
|
+
});
|
|
610
|
+
};
|
|
611
|
+
return runtime;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const CFENV_HINT =
|
|
615
|
+
'This photon imports `CloudflareEnv` from "@portel/photon" but no CF ' +
|
|
616
|
+
'env was attached. Run via `photon host run` (miniflare-backed) or ' +
|
|
617
|
+
'deploy with `photon host deploy cloudflare`. Outside CF, this ' +
|
|
618
|
+
'photon\'s CF-dependent methods cannot execute.';
|
|
619
|
+
|
|
620
|
+
function makeThrowingCloudflareEnv(): Record<string, unknown> {
|
|
621
|
+
// Engine-internal property accesses (constructor lookup, async-iterator
|
|
622
|
+
// probing, `await` thenable check, JSON.stringify) must not throw —
|
|
623
|
+
// otherwise, harmless reflection from runtime utilities like
|
|
624
|
+
// `wireReactiveCollections` blows up the photon load before any user
|
|
625
|
+
// code runs. We satisfy those safely and only throw when actual
|
|
626
|
+
// user code reads a binding name.
|
|
627
|
+
const safePassthrough = new Set<string | symbol>([
|
|
628
|
+
'constructor',
|
|
629
|
+
'then',
|
|
630
|
+
'toJSON',
|
|
631
|
+
'toString',
|
|
632
|
+
'valueOf',
|
|
633
|
+
Symbol.toPrimitive,
|
|
634
|
+
Symbol.toStringTag,
|
|
635
|
+
Symbol.iterator,
|
|
636
|
+
Symbol.asyncIterator,
|
|
637
|
+
]);
|
|
638
|
+
return new Proxy(Object.create(null), {
|
|
639
|
+
get(_target, prop) {
|
|
640
|
+
if (safePassthrough.has(prop)) {
|
|
641
|
+
if (prop === 'toString' || prop === Symbol.toPrimitive) {
|
|
642
|
+
return () => '[unconfigured CloudflareEnv]';
|
|
643
|
+
}
|
|
644
|
+
return undefined;
|
|
645
|
+
}
|
|
646
|
+
throw new Error(`CloudflareEnv.${String(prop)} accessed but ${CFENV_HINT}`);
|
|
647
|
+
},
|
|
648
|
+
has(_target, prop) {
|
|
649
|
+
return !safePassthrough.has(prop);
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
469
654
|
// ═══════════════════════════════════════════════════════════════════
|
|
470
655
|
// Reactive collection wiring
|
|
471
656
|
// ═══════════════════════════════════════════════════════════════════
|
package/src/schema-extractor.ts
CHANGED
|
@@ -33,6 +33,12 @@ function warnHandlePrefixOnce(methodName: string): void {
|
|
|
33
33
|
);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export interface HttpRoute {
|
|
37
|
+
method: 'GET' | 'POST';
|
|
38
|
+
path: string;
|
|
39
|
+
handler: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
export interface ExtractedMetadata {
|
|
37
43
|
tools: ExtractedSchema[];
|
|
38
44
|
templates: TemplateInfo[];
|
|
@@ -48,8 +54,11 @@ export interface ExtractedMetadata {
|
|
|
48
54
|
* - 'required': all methods require authenticated caller
|
|
49
55
|
* - 'optional': caller populated if token present, anonymous allowed
|
|
50
56
|
* - string URL: OIDC provider URL (implies required)
|
|
57
|
+
* - 'cf-access': use Cloudflare Access JWT to identify caller
|
|
51
58
|
*/
|
|
52
|
-
auth?: 'required' | 'optional' | string;
|
|
59
|
+
auth?: 'required' | 'optional' | 'cf-access' | string;
|
|
60
|
+
/** HTTP routes declared with @get or @post on individual methods */
|
|
61
|
+
httpRoutes?: HttpRoute[];
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
/**
|
|
@@ -93,6 +102,9 @@ export class SchemaExtractor {
|
|
|
93
102
|
// MCP OAuth auth requirement (from @auth tag)
|
|
94
103
|
let auth: 'required' | 'optional' | string | undefined;
|
|
95
104
|
|
|
105
|
+
// HTTP routes from @get / @post method-level tags
|
|
106
|
+
const httpRoutes: HttpRoute[] = [];
|
|
107
|
+
|
|
96
108
|
try {
|
|
97
109
|
// If source doesn't contain a class declaration, wrap it in one
|
|
98
110
|
let sourceToParse = source;
|
|
@@ -156,6 +168,18 @@ export class SchemaExtractor {
|
|
|
156
168
|
return;
|
|
157
169
|
}
|
|
158
170
|
|
|
171
|
+
// @get /path or @post /path — HTTP-only route, not an MCP tool
|
|
172
|
+
const getMatch = jsdoc.match(/@get\s+(\/\S*)/i);
|
|
173
|
+
const postMatch = jsdoc.match(/@post\s+(\/\S*)/i);
|
|
174
|
+
if (getMatch || postMatch) {
|
|
175
|
+
httpRoutes.push({
|
|
176
|
+
method: getMatch ? 'GET' : 'POST',
|
|
177
|
+
path: (getMatch ?? postMatch)![1],
|
|
178
|
+
handler: methodName,
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
159
183
|
// Check if this is an async generator method (has asterisk token)
|
|
160
184
|
const isGenerator = member.asteriskToken !== undefined;
|
|
161
185
|
|
|
@@ -562,6 +586,11 @@ export class SchemaExtractor {
|
|
|
562
586
|
result.auth = auth;
|
|
563
587
|
}
|
|
564
588
|
|
|
589
|
+
// Include HTTP routes if any
|
|
590
|
+
if (httpRoutes.length > 0) {
|
|
591
|
+
result.httpRoutes = httpRoutes;
|
|
592
|
+
}
|
|
593
|
+
|
|
565
594
|
return result;
|
|
566
595
|
}
|
|
567
596
|
|
|
@@ -1980,17 +2009,24 @@ export class SchemaExtractor {
|
|
|
1980
2009
|
}
|
|
1981
2010
|
|
|
1982
2011
|
/**
|
|
1983
|
-
* Check if JSDoc
|
|
2012
|
+
* Check if a method's JSDoc marks it as an MCP prompt template.
|
|
2013
|
+
* Canonical form: `@prompt`. Legacy form `@Template` still accepted
|
|
2014
|
+
* for backward compatibility with photons authored before the rename.
|
|
1984
2015
|
*/
|
|
1985
2016
|
private hasTemplateTag(jsdocContent: string): boolean {
|
|
1986
|
-
return /@Template/i.test(jsdocContent);
|
|
2017
|
+
return /@(?:Template|prompt)\b/i.test(jsdocContent);
|
|
1987
2018
|
}
|
|
1988
2019
|
|
|
1989
2020
|
/**
|
|
1990
|
-
* Check if JSDoc
|
|
2021
|
+
* Check if a method's JSDoc marks it as a dynamic MCP resource resolver.
|
|
2022
|
+
* Canonical form: `@resource <uri-template>`. Legacy form `@Static`
|
|
2023
|
+
* still accepted. Disambiguation from the class-level static-file form
|
|
2024
|
+
* (`@resource <id> <path>`) is by argument shape: the class-level form
|
|
2025
|
+
* requires `<id>` followed by a path starting with `./` or `/`, which
|
|
2026
|
+
* the URI-template form never matches.
|
|
1991
2027
|
*/
|
|
1992
2028
|
private hasStaticTag(jsdocContent: string): boolean {
|
|
1993
|
-
return /@Static/i.test(jsdocContent);
|
|
2029
|
+
return /@(?:Static|resource)\b/i.test(jsdocContent);
|
|
1994
2030
|
}
|
|
1995
2031
|
|
|
1996
2032
|
/**
|
|
@@ -2448,11 +2484,12 @@ export class SchemaExtractor {
|
|
|
2448
2484
|
}
|
|
2449
2485
|
|
|
2450
2486
|
/**
|
|
2451
|
-
* Extract URI pattern from
|
|
2452
|
-
*
|
|
2487
|
+
* Extract URI pattern from a method's resource annotation.
|
|
2488
|
+
* Canonical: `@resource github://repos/{owner}/{repo}/readme`
|
|
2489
|
+
* Legacy: `@Static github://repos/{owner}/{repo}/readme`
|
|
2453
2490
|
*/
|
|
2454
2491
|
private extractStaticURI(jsdocContent: string): string | null {
|
|
2455
|
-
const match = jsdocContent.match(/@Static\s+([\w:\/\{\}\-_.]+)/i);
|
|
2492
|
+
const match = jsdocContent.match(/@(?:Static|resource)\s+([\w:\/\{\}\-_.]+)/i);
|
|
2456
2493
|
return match ? match[1].trim() : null;
|
|
2457
2494
|
}
|
|
2458
2495
|
|
|
@@ -2851,6 +2888,30 @@ export class SchemaExtractor {
|
|
|
2851
2888
|
const photonMap = new Map(photonDeps.map(d => [d.name, d]));
|
|
2852
2889
|
|
|
2853
2890
|
return params.map(param => {
|
|
2891
|
+
// Framework-recognized typed injections (matched on the parameter
|
|
2892
|
+
// type, not the parameter name). Win ahead of every other rule
|
|
2893
|
+
// because they are unambiguous: a user importing `Photon` /
|
|
2894
|
+
// `Cloudflare` / `CloudflareEnv` and typing a constructor param
|
|
2895
|
+
// with one of those names is asking the loader to wire it.
|
|
2896
|
+
if (isPhotonRuntimeType(param.type)) {
|
|
2897
|
+
return {
|
|
2898
|
+
param,
|
|
2899
|
+
injectionType: 'photonRuntime' as const,
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
if (isCloudflareType(param.type)) {
|
|
2903
|
+
return {
|
|
2904
|
+
param,
|
|
2905
|
+
injectionType: 'cloudflare' as const,
|
|
2906
|
+
};
|
|
2907
|
+
}
|
|
2908
|
+
if (isCloudflareEnvType(param.type)) {
|
|
2909
|
+
return {
|
|
2910
|
+
param,
|
|
2911
|
+
injectionType: 'cloudflareEnv' as const,
|
|
2912
|
+
};
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2854
2915
|
// Primitives → env var
|
|
2855
2916
|
if (param.isPrimitive) {
|
|
2856
2917
|
const envVarName = this.toEnvVarName(mcpName, param.name);
|
|
@@ -3127,7 +3188,23 @@ export class SchemaExtractor {
|
|
|
3127
3188
|
/**
|
|
3128
3189
|
* Capability types that can be auto-detected from source code
|
|
3129
3190
|
*/
|
|
3130
|
-
export type PhotonCapability =
|
|
3191
|
+
export type PhotonCapability =
|
|
3192
|
+
| 'emit'
|
|
3193
|
+
| 'memory'
|
|
3194
|
+
| 'call'
|
|
3195
|
+
| 'mcp'
|
|
3196
|
+
| 'lock'
|
|
3197
|
+
| 'instanceMeta'
|
|
3198
|
+
| 'allInstances'
|
|
3199
|
+
| 'caller'
|
|
3200
|
+
// Forgiving auto-inject: when these are detected on a class that
|
|
3201
|
+
// didn't declare a constructor param of the matching type, the loader
|
|
3202
|
+
// assigns the matching field on the instance after construction so
|
|
3203
|
+
// `this.cf.kv()` / `this.cfEnv.MY_KV.put(...)` work without the user
|
|
3204
|
+
// having to wire the import + ctor param themselves. The diagnostic
|
|
3205
|
+
// remains clear when the matching CF runtime isn't actually attached.
|
|
3206
|
+
| 'cloudflare'
|
|
3207
|
+
| 'cloudflareEnv';
|
|
3131
3208
|
|
|
3132
3209
|
/**
|
|
3133
3210
|
* Match a `this`-like base in source code. Covers:
|
|
@@ -3157,6 +3234,34 @@ function memberAccess(name: string, trailing: '\\(' | '\\b'): RegExp {
|
|
|
3157
3234
|
return new RegExp(`${THIS_BASE}\\s*\\.\\s*${name}\\s*${trailing}`);
|
|
3158
3235
|
}
|
|
3159
3236
|
|
|
3237
|
+
/**
|
|
3238
|
+
* Constructor-param type matchers for the framework-injected types.
|
|
3239
|
+
* The schema extractor sees parameter types as raw text (e.g. `"Photon"`,
|
|
3240
|
+
* `"Cloudflare"`, `"CloudflareEnv<MyBindings>"`), so we match by string.
|
|
3241
|
+
*
|
|
3242
|
+
* Optional modifiers / unions like `Photon | undefined` are accepted so
|
|
3243
|
+
* authors who write `private photon?: Photon` get the same injection.
|
|
3244
|
+
*/
|
|
3245
|
+
function stripNullish(type: string): string {
|
|
3246
|
+
return type
|
|
3247
|
+
.trim()
|
|
3248
|
+
.replace(/\|\s*undefined\b/g, '')
|
|
3249
|
+
.replace(/\|\s*null\b/g, '')
|
|
3250
|
+
.trim();
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
export function isPhotonRuntimeType(type: string): boolean {
|
|
3254
|
+
return /^Photon$/.test(stripNullish(type));
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
export function isCloudflareType(type: string): boolean {
|
|
3258
|
+
return /^Cloudflare$/.test(stripNullish(type));
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
export function isCloudflareEnvType(type: string): boolean {
|
|
3262
|
+
return /^CloudflareEnv(\s*<[\s\S]+>)?$/.test(stripNullish(type));
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3160
3265
|
/**
|
|
3161
3266
|
* Detect capabilities used by a Photon from its source code.
|
|
3162
3267
|
*
|
|
@@ -3178,5 +3283,10 @@ export function detectCapabilities(source: string): Set<PhotonCapability> {
|
|
|
3178
3283
|
if (memberAccess('instanceMeta', '\\b').test(source)) caps.add('instanceMeta');
|
|
3179
3284
|
if (memberAccess('allInstances', '\\(').test(source)) caps.add('allInstances');
|
|
3180
3285
|
if (memberAccess('caller', '\\b').test(source)) caps.add('caller');
|
|
3286
|
+
// Forgiving auto-inject signals — surface as separate capabilities so
|
|
3287
|
+
// the loader can populate `instance.cf` / `instance.cfEnv` for plain
|
|
3288
|
+
// classes that reference them without the explicit constructor param.
|
|
3289
|
+
if (memberAccess('cf', '\\b').test(source)) caps.add('cloudflare');
|
|
3290
|
+
if (memberAccess('cfEnv', '\\b').test(source)) caps.add('cloudflareEnv');
|
|
3181
3291
|
return caps;
|
|
3182
3292
|
}
|
package/src/types.ts
CHANGED
|
@@ -313,7 +313,22 @@ export interface ConstructorParam {
|
|
|
313
313
|
/**
|
|
314
314
|
* Injection type for constructor parameters
|
|
315
315
|
*/
|
|
316
|
-
export type InjectionType =
|
|
316
|
+
export type InjectionType =
|
|
317
|
+
| 'env'
|
|
318
|
+
| 'mcp'
|
|
319
|
+
| 'photon'
|
|
320
|
+
| 'state'
|
|
321
|
+
// Constructor param typed `Photon` from @portel/photon — loader injects
|
|
322
|
+
// a Photon instance configured identically to the host photon.
|
|
323
|
+
| 'photonRuntime'
|
|
324
|
+
// Constructor param typed `Cloudflare` — loader injects a wrapped CF
|
|
325
|
+
// surface (auto-named bindings) backed by miniflare locally or by the
|
|
326
|
+
// deployed Worker `env`.
|
|
327
|
+
| 'cloudflare'
|
|
328
|
+
// Constructor param typed `CloudflareEnv` / `CloudflareEnv<T>` — loader
|
|
329
|
+
// injects the raw Worker `env`. Escape hatch for service bindings
|
|
330
|
+
// and exotic features the wrapped surface doesn't cover.
|
|
331
|
+
| 'cloudflareEnv';
|
|
317
332
|
|
|
318
333
|
/**
|
|
319
334
|
* Resolved injection info for a constructor parameter
|