@llui/vite-plugin 0.0.31 → 0.0.34
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 +8 -9
- package/dist/binding-descriptors.d.ts +98 -16
- package/dist/binding-descriptors.d.ts.map +1 -1
- package/dist/binding-descriptors.js +319 -61
- package/dist/binding-descriptors.js.map +1 -1
- package/dist/cross-file-resolver.d.ts +109 -0
- package/dist/cross-file-resolver.d.ts.map +1 -0
- package/dist/cross-file-resolver.js +457 -0
- package/dist/cross-file-resolver.js.map +1 -0
- package/dist/index.d.ts +8 -32
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +246 -37
- package/dist/index.js.map +1 -1
- package/dist/msg-annotations.d.ts +47 -5
- package/dist/msg-annotations.d.ts.map +1 -1
- package/dist/msg-annotations.js +85 -9
- package/dist/msg-annotations.js.map +1 -1
- package/dist/msg-schema.d.ts +81 -5
- package/dist/msg-schema.d.ts.map +1 -1
- package/dist/msg-schema.js +201 -13
- package/dist/msg-schema.js.map +1 -1
- package/dist/transform.d.ts +47 -1
- package/dist/transform.d.ts.map +1 -1
- package/dist/transform.js +155 -61
- package/dist/transform.js.map +1 -1
- package/package.json +1 -1
- package/dist/diagnostics.d.ts +0 -14
- package/dist/diagnostics.js +0 -846
- package/dist/diagnostics.js.map +0 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { Plugin } from 'vite';
|
|
2
|
-
import { type DiagnosticRule } from './diagnostics.js';
|
|
3
|
-
export type { DiagnosticRule } from './diagnostics.js';
|
|
4
2
|
export interface LluiPluginOptions {
|
|
5
3
|
/**
|
|
6
4
|
* Port for the MCP debug bridge. In dev mode, the runtime relay connects
|
|
@@ -17,28 +15,6 @@ export interface LluiPluginOptions {
|
|
|
17
15
|
* silently skips the connection — no retry noise.
|
|
18
16
|
*/
|
|
19
17
|
mcpPort?: number | false;
|
|
20
|
-
/**
|
|
21
|
-
* Treat every compiler diagnostic as a build error.
|
|
22
|
-
*
|
|
23
|
-
* Default `false` — diagnostics are emitted via rollup's `this.warn` and
|
|
24
|
-
* can be ignored. Set to `true` in CI so lint-style regressions (namespace
|
|
25
|
-
* imports, bitmask overflow, spread-in-children, `.map()` on state, etc.)
|
|
26
|
-
* fail the build without requiring a custom `build.rollupOptions.onwarn`
|
|
27
|
-
* handler.
|
|
28
|
-
*/
|
|
29
|
-
failOnWarning?: boolean;
|
|
30
|
-
/**
|
|
31
|
-
* Silence specific diagnostic rules without disabling the whole lint
|
|
32
|
-
* pass. Each message is tagged with a rule name (shown in brackets at
|
|
33
|
-
* the start of every warning, e.g. `[spread-in-children]`). Listing
|
|
34
|
-
* a rule here drops all diagnostics with that tag before rollup sees
|
|
35
|
-
* them — so they don't fire via `this.warn` and don't fail the build
|
|
36
|
-
* even when `failOnWarning` is enabled.
|
|
37
|
-
*
|
|
38
|
-
* The valid rule names are enumerated by the `DiagnosticRule` type
|
|
39
|
-
* re-exported from this module. Unknown rule names are ignored.
|
|
40
|
-
*/
|
|
41
|
-
disabledWarnings?: readonly DiagnosticRule[];
|
|
42
18
|
/**
|
|
43
19
|
* Emit `[llui]`-prefixed `console.info` logs for every transformed
|
|
44
20
|
* component file — state-path bit assignments, mask injections, and
|
|
@@ -66,13 +42,13 @@ export interface LluiPluginOptions {
|
|
|
66
42
|
*/
|
|
67
43
|
agent?: boolean | AgentPluginConfig;
|
|
68
44
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Reserved for future agent-server config. Empty today — opaque tokens
|
|
47
|
+
* (post-0.0.35) need no signing key, and the dev server hard-codes the
|
|
48
|
+
* identity resolver to `'dev-user'`. The shape is kept so callers can
|
|
49
|
+
* pass `agent: { ... }` and we can grow options without churning the
|
|
50
|
+
* public type.
|
|
51
|
+
*/
|
|
52
|
+
export type AgentPluginConfig = Record<string, never>;
|
|
77
53
|
export default function llui(options?: LluiPluginOptions): Plugin;
|
|
78
54
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAA;AAsOjD,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;IAExB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;;;;;;;;;;;;;;;OAgBG;IACH,KAAK,CAAC,EAAE,OAAO,GAAG,iBAAiB,CAAA;CACpC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAA;AAuMrD,MAAM,CAAC,OAAO,UAAU,IAAI,CAAC,OAAO,GAAE,iBAAsB,GAAG,MAAM,CAqWpE"}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,182 @@
|
|
|
1
1
|
import MagicString from 'magic-string';
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync, watch as fsWatch } from 'node:fs';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
3
4
|
import { dirname, relative, resolve } from 'node:path';
|
|
4
5
|
import { createRequire } from 'node:module';
|
|
5
6
|
import { spawn } from 'node:child_process';
|
|
6
|
-
import { transformLlui, transformUseClientSsr, hasUseClientDirective } from './transform.js';
|
|
7
|
-
import {
|
|
7
|
+
import { transformLlui, transformUseClientSsr, hasUseClientDirective, } from './transform.js';
|
|
8
|
+
import { findTypeSource, readComponentTypeArgNames, extractMsgAnnotationsCrossFile, extractDiscriminatedUnionSchemaCrossFile, } from './cross-file-resolver.js';
|
|
9
|
+
import ts from 'typescript';
|
|
10
|
+
/**
|
|
11
|
+
* Pre-resolution step run before `transformLlui`. Scans the source for
|
|
12
|
+
* `component<State, Msg, Effect>(...)` calls; for each type argument that
|
|
13
|
+
* is an identifier (the common case), walks imports and re-exports to
|
|
14
|
+
* find the source file declaring that alias. The result is plumbed into
|
|
15
|
+
* `transformLlui` so the schema/annotation extractors operate on the
|
|
16
|
+
* declaring file's source instead of silently returning `null` when the
|
|
17
|
+
* type lives in a separate file.
|
|
18
|
+
*
|
|
19
|
+
* Returns `undefined` (no external sources) when:
|
|
20
|
+
* - No `component<...>()` call is in the file
|
|
21
|
+
* - No type arguments are identifiers we can chase
|
|
22
|
+
* - All type arguments are declared locally (the resolver returns the
|
|
23
|
+
* same source we already have, so external sources are redundant)
|
|
24
|
+
*
|
|
25
|
+
* `resolveModule` comes from Rollup's `this.resolve()`; we wrap it to
|
|
26
|
+
* return the absolute id (or null when unresolved) and read the source
|
|
27
|
+
* via `fs/promises.readFile`.
|
|
28
|
+
*/
|
|
29
|
+
async function preResolveTypeSources(source, filePath, rollupResolve) {
|
|
30
|
+
// Cheap filter: nothing to resolve unless the file contains a
|
|
31
|
+
// component<...>() call. Avoids parsing every TS file in the project.
|
|
32
|
+
if (!/\bcomponent\s*</.test(source))
|
|
33
|
+
return undefined;
|
|
34
|
+
// Find the first component<...>() call and read its type arg names.
|
|
35
|
+
// Multiple component() calls in one file would each technically need
|
|
36
|
+
// their own type-arg lookup; we resolve based on the first call and
|
|
37
|
+
// accept the (rare) edge case where two component() calls in one file
|
|
38
|
+
// use different non-local Msg types. The lint rule catches divergence.
|
|
39
|
+
const sf = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
|
|
40
|
+
const args = findFirstComponentTypeArgs(sf);
|
|
41
|
+
if (!args)
|
|
42
|
+
return undefined;
|
|
43
|
+
const ctx = {
|
|
44
|
+
resolveModule: async (spec, importer) => {
|
|
45
|
+
const result = await rollupResolve(spec, importer);
|
|
46
|
+
if (!result || result.external)
|
|
47
|
+
return null;
|
|
48
|
+
// Rollup ids can include query/hash suffixes for virtual modules;
|
|
49
|
+
// strip those so fs.readFile sees a real path. Also skip files
|
|
50
|
+
// outside our control (node_modules) — we don't want to follow
|
|
51
|
+
// imports into third-party packages just to scrape types.
|
|
52
|
+
const idStripped = result.id.split('?')[0]?.split('#')[0];
|
|
53
|
+
if (!idStripped)
|
|
54
|
+
return null;
|
|
55
|
+
if (idStripped.includes('/node_modules/'))
|
|
56
|
+
return null;
|
|
57
|
+
return idStripped;
|
|
58
|
+
},
|
|
59
|
+
readSource: async (p) => {
|
|
60
|
+
return await readFile(p, 'utf8');
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
// Helper to resolve one type-arg name into an external source if it
|
|
64
|
+
// isn't declared locally (or if the resolver chases through imports).
|
|
65
|
+
const resolve = async (typeName) => {
|
|
66
|
+
if (!typeName)
|
|
67
|
+
return undefined;
|
|
68
|
+
const found = await findTypeSource(typeName, source, filePath, ctx);
|
|
69
|
+
if (!found)
|
|
70
|
+
return undefined;
|
|
71
|
+
// If the alias was declared locally, the existing extractor path
|
|
72
|
+
// already handles it — no need to populate external sources.
|
|
73
|
+
if (found.filePath === filePath)
|
|
74
|
+
return undefined;
|
|
75
|
+
return { source: found.source, typeName: found.localName };
|
|
76
|
+
};
|
|
77
|
+
const [state, msg, effect] = await Promise.all([
|
|
78
|
+
resolve(args.state),
|
|
79
|
+
resolve(args.msg),
|
|
80
|
+
resolve(args.effect),
|
|
81
|
+
]);
|
|
82
|
+
if (!state && !msg && !effect)
|
|
83
|
+
return undefined;
|
|
84
|
+
return { state, msg, effect };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Cross-file + composition-aware schema extraction. The extractors
|
|
88
|
+
* follow imports/re-exports AND walk into TypeReferences inside Msg /
|
|
89
|
+
* Effect unions, so a developer who organises types as
|
|
90
|
+
* `type Msg = ImportedFoo | { type: 'extra' }` gets every variant in
|
|
91
|
+
* `__msgAnnotations` and `__msgSchema`. Without this step the
|
|
92
|
+
* file-local sync extractors would silently emit half-annotations
|
|
93
|
+
* (only the inline TypeLiteral members) — the worst kind of failure
|
|
94
|
+
* mode because the build appears to succeed.
|
|
95
|
+
*
|
|
96
|
+
* Returns `undefined` (no pre-extraction) when there's no
|
|
97
|
+
* `component()` call to resolve types for.
|
|
98
|
+
*/
|
|
99
|
+
async function preExtractCompositional(source, filePath, rollupResolve) {
|
|
100
|
+
if (!/\bcomponent\s*</.test(source))
|
|
101
|
+
return undefined;
|
|
102
|
+
const sf = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
|
|
103
|
+
const args = findFirstComponentTypeArgs(sf);
|
|
104
|
+
if (!args)
|
|
105
|
+
return undefined;
|
|
106
|
+
// No identifier type args at all → nothing for the resolver to chase.
|
|
107
|
+
if (!args.msg && !args.effect && !args.state)
|
|
108
|
+
return undefined;
|
|
109
|
+
const ctx = {
|
|
110
|
+
resolveModule: async (spec, importer) => {
|
|
111
|
+
const result = await rollupResolve(spec, importer);
|
|
112
|
+
if (!result || result.external)
|
|
113
|
+
return null;
|
|
114
|
+
const idStripped = result.id.split('?')[0]?.split('#')[0];
|
|
115
|
+
if (!idStripped)
|
|
116
|
+
return null;
|
|
117
|
+
if (idStripped.includes('/node_modules/'))
|
|
118
|
+
return null;
|
|
119
|
+
return idStripped;
|
|
120
|
+
},
|
|
121
|
+
readSource: async (p) => readFile(p, 'utf8'),
|
|
122
|
+
};
|
|
123
|
+
const [msgAnnotations, msgSchema, effectSchema] = await Promise.all([
|
|
124
|
+
args.msg
|
|
125
|
+
? extractMsgAnnotationsCrossFile(source, args.msg, filePath, ctx)
|
|
126
|
+
: Promise.resolve(null),
|
|
127
|
+
args.msg
|
|
128
|
+
? extractDiscriminatedUnionSchemaCrossFile(source, args.msg, filePath, ctx)
|
|
129
|
+
: Promise.resolve(null),
|
|
130
|
+
args.effect
|
|
131
|
+
? extractDiscriminatedUnionSchemaCrossFile(source, args.effect, filePath, ctx)
|
|
132
|
+
: Promise.resolve(null),
|
|
133
|
+
]);
|
|
134
|
+
// Only return a populated payload when we actually extracted
|
|
135
|
+
// something useful. Returning `undefined` lets transformLlui fall
|
|
136
|
+
// back to its file-local extractors, which is the right behavior
|
|
137
|
+
// for the (rare) case where every type the resolver sees is
|
|
138
|
+
// unreachable.
|
|
139
|
+
if (msgAnnotations === null && msgSchema === null && effectSchema === null)
|
|
140
|
+
return undefined;
|
|
141
|
+
// Note: state schema isn't a discriminated union, so composition
|
|
142
|
+
// doesn't apply. We leave state on the simpler `typeSources` path
|
|
143
|
+
// (already plumbed through preResolveTypeSources) which the
|
|
144
|
+
// file-local `extractStateSchema` consumes.
|
|
145
|
+
const out = {};
|
|
146
|
+
if (msgAnnotations !== null)
|
|
147
|
+
out.msgAnnotations = msgAnnotations;
|
|
148
|
+
if (msgSchema !== null)
|
|
149
|
+
out.msgSchema = msgSchema;
|
|
150
|
+
if (effectSchema !== null)
|
|
151
|
+
out.effectSchema = effectSchema;
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
function findFirstComponentTypeArgs(sf) {
|
|
155
|
+
let result = null;
|
|
156
|
+
const visit = (node) => {
|
|
157
|
+
if (result)
|
|
158
|
+
return true;
|
|
159
|
+
if (ts.isCallExpression(node) &&
|
|
160
|
+
ts.isIdentifier(node.expression) &&
|
|
161
|
+
node.expression.text === 'component' &&
|
|
162
|
+
node.typeArguments) {
|
|
163
|
+
result = readComponentTypeArgNames(node);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
let stopped = false;
|
|
167
|
+
ts.forEachChild(node, (child) => {
|
|
168
|
+
if (stopped)
|
|
169
|
+
return;
|
|
170
|
+
if (visit(child))
|
|
171
|
+
stopped = true;
|
|
172
|
+
});
|
|
173
|
+
return stopped;
|
|
174
|
+
};
|
|
175
|
+
ts.forEachChild(sf, (child) => {
|
|
176
|
+
visit(child);
|
|
177
|
+
});
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
8
180
|
/**
|
|
9
181
|
* Locate the workspace root so we share the MCP active marker file
|
|
10
182
|
* with @llui/mcp regardless of which subdirectory the dev server runs in.
|
|
@@ -67,7 +239,7 @@ function resolveMcpCliPath(root) {
|
|
|
67
239
|
* construct an agent server instance. Returns null if @llui/agent isn't
|
|
68
240
|
* installed — the plugin degrades to "prod schema emission only" mode.
|
|
69
241
|
*/
|
|
70
|
-
async function loadAgentServer(appRoot,
|
|
242
|
+
async function loadAgentServer(appRoot, _cfg) {
|
|
71
243
|
let serverModule;
|
|
72
244
|
try {
|
|
73
245
|
// Walk up from the app root to find node_modules/@llui/agent. Works
|
|
@@ -90,10 +262,11 @@ async function loadAgentServer(appRoot, cfg) {
|
|
|
90
262
|
(e instanceof Error ? e.message : String(e)));
|
|
91
263
|
return null;
|
|
92
264
|
}
|
|
93
|
-
|
|
94
|
-
|
|
265
|
+
// The pre-0.0.35 agent server required an HMAC signingKey for JWT
|
|
266
|
+
// tokens. The opaque-token rewrite removed that option; the dev
|
|
267
|
+
// server here just calls the factory with no auth config — the
|
|
268
|
+
// in-memory token store is the source of truth.
|
|
95
269
|
return serverModule.createLluiAgentServer({
|
|
96
|
-
signingKey,
|
|
97
270
|
identityResolver: async () => 'dev-user',
|
|
98
271
|
});
|
|
99
272
|
}
|
|
@@ -106,12 +279,31 @@ function registerAgentMiddleware(server, agent) {
|
|
|
106
279
|
// Connect-style middleware. Vite's middleware chain runs in order, so
|
|
107
280
|
// synchronous registration during configureServer places us ahead of
|
|
108
281
|
// Vite's catch-all fallback.
|
|
282
|
+
//
|
|
283
|
+
// Dual-path: handle the canonical `/agent/*` (every project) AND
|
|
284
|
+
// `/cdn-cgi/agent/*` (defensive — Cloudflare's `@cloudflare/vite-plugin`
|
|
285
|
+
// routes everything except `/cdn-cgi/*` to the worker, which means
|
|
286
|
+
// canonical `/agent/*` paths are shadowed in cloudflare-vite projects).
|
|
287
|
+
// The cdn-cgi prefix is stripped before forwarding so the agent
|
|
288
|
+
// server's router sees its own canonical paths regardless of which
|
|
289
|
+
// public URL the client used. This matches the dual-path strategy
|
|
290
|
+
// used for `/__llui_mcp_status`.
|
|
109
291
|
server.middlewares.use((req, res, next) => {
|
|
110
292
|
const url = req.url ?? '/';
|
|
111
|
-
|
|
293
|
+
let stripped = null;
|
|
294
|
+
if (url.startsWith('/agent/') || url === '/agent')
|
|
295
|
+
stripped = url;
|
|
296
|
+
else if (url.startsWith('/cdn-cgi/agent/') || url === '/cdn-cgi/agent') {
|
|
297
|
+
stripped = url.slice('/cdn-cgi'.length);
|
|
298
|
+
}
|
|
299
|
+
if (stripped === null) {
|
|
112
300
|
next();
|
|
113
301
|
return;
|
|
114
302
|
}
|
|
303
|
+
// Rewrite the request URL in-place so handleAgentRequest's path
|
|
304
|
+
// matching sees `/agent/*`. Connect middleware can mutate req.url
|
|
305
|
+
// for downstream handlers; we own the request from here.
|
|
306
|
+
req.url = stripped;
|
|
115
307
|
void handleAgentRequest(req, res, agent.router).catch((e) => {
|
|
116
308
|
console.error('[llui] agent middleware error:', e);
|
|
117
309
|
next(e);
|
|
@@ -119,14 +311,18 @@ function registerAgentMiddleware(server, agent) {
|
|
|
119
311
|
});
|
|
120
312
|
// WS upgrade: only /agent/ws goes to the agent. Vite's own HMR upgrade
|
|
121
313
|
// uses a different path and runs as a separate listener on the same
|
|
122
|
-
// event, so this filter keeps both coexisting.
|
|
314
|
+
// event, so this filter keeps both coexisting. Same dual-path
|
|
315
|
+
// accommodation as the HTTP middleware — the WS-upgrade path doesn't
|
|
316
|
+
// actually matter to most cloudflare setups (the worker handles WS
|
|
317
|
+
// upgrades natively), but keeping the parity simplifies the mental
|
|
318
|
+
// model for ops.
|
|
123
319
|
server.httpServer?.on('upgrade', (req, socket, head) => {
|
|
124
320
|
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
125
|
-
if (url.pathname === '/agent/ws') {
|
|
321
|
+
if (url.pathname === '/agent/ws' || url.pathname === '/cdn-cgi/agent/ws') {
|
|
126
322
|
agent.wsUpgrade(req, socket, head);
|
|
127
323
|
}
|
|
128
324
|
});
|
|
129
|
-
console.info('[llui] agent dev endpoints active: POST /agent/mint, WS /agent/ws, LAP /agent/lap/v1/*');
|
|
325
|
+
console.info('[llui] agent dev endpoints active: POST /agent/mint, WS /agent/ws, LAP /agent/lap/v1/* (also reachable under /cdn-cgi/agent/* for cloudflare-vite parity)');
|
|
130
326
|
}
|
|
131
327
|
/**
|
|
132
328
|
* Walk up from `start` looking for `node_modules/<pkgName>`. Returns the
|
|
@@ -187,8 +383,6 @@ export default function llui(options = {}) {
|
|
|
187
383
|
let mcpMode = 'disabled';
|
|
188
384
|
let mcpCliPath = null;
|
|
189
385
|
let mcpChild = null;
|
|
190
|
-
const failOnWarning = options.failOnWarning === true;
|
|
191
|
-
const disabledWarnings = new Set(options.disabledWarnings ?? []);
|
|
192
386
|
const verbose = options.verbose === true;
|
|
193
387
|
const agent = options.agent ?? false;
|
|
194
388
|
const agentConfig = typeof agent === 'object' ? agent : {};
|
|
@@ -342,7 +536,17 @@ export default function llui(options = {}) {
|
|
|
342
536
|
// the import.meta.hot listener registers get dropped — and lets
|
|
343
537
|
// the browser connect to the actual port (which may differ from
|
|
344
538
|
// the compile-time default if MCP was started with LLUI_MCP_PORT).
|
|
345
|
-
|
|
539
|
+
//
|
|
540
|
+
// Two paths register the same handler:
|
|
541
|
+
// * `/__llui_mcp_status` — canonical, served from any Vite
|
|
542
|
+
// project.
|
|
543
|
+
// * `/cdn-cgi/llui_mcp_status` — fallback for projects that
|
|
544
|
+
// bundle `@cloudflare/vite-plugin`. The cloudflare plugin
|
|
545
|
+
// intercepts every HTTP request in `configureServer` and
|
|
546
|
+
// routes it to the worker, except `/cdn-cgi/*` which it
|
|
547
|
+
// explicitly lets through. Without this fallback, MCP
|
|
548
|
+
// auto-discovery silently fails under workerd.
|
|
549
|
+
const mcpStatusHandler = (_req, res) => {
|
|
346
550
|
const marker = readMcpMarker();
|
|
347
551
|
if (marker === null) {
|
|
348
552
|
res.statusCode = 404;
|
|
@@ -352,7 +556,9 @@ export default function llui(options = {}) {
|
|
|
352
556
|
res.statusCode = 200;
|
|
353
557
|
res.setHeader('content-type', 'application/json');
|
|
354
558
|
res.end(JSON.stringify({ port: marker.port }));
|
|
355
|
-
}
|
|
559
|
+
};
|
|
560
|
+
server.middlewares.use('/__llui_mcp_status', mcpStatusHandler);
|
|
561
|
+
server.middlewares.use('/cdn-cgi/llui_mcp_status', mcpStatusHandler);
|
|
356
562
|
// Watch the marker file for create/delete. fs.watch on the parent
|
|
357
563
|
// directory catches both events; the file itself may not exist
|
|
358
564
|
// when we start watching.
|
|
@@ -436,7 +642,7 @@ export default function llui(options = {}) {
|
|
|
436
642
|
// themselves — configureServer also fires in middleware mode, but
|
|
437
643
|
// there server.httpServer is null so the upgrade hook is a no-op.
|
|
438
644
|
},
|
|
439
|
-
transform(code, id, options) {
|
|
645
|
+
async transform(code, id, options) {
|
|
440
646
|
if (!id.endsWith('.ts') && !id.endsWith('.tsx'))
|
|
441
647
|
return;
|
|
442
648
|
// `'use client'` directive — SSR builds replace the module with a
|
|
@@ -455,28 +661,31 @@ export default function llui(options = {}) {
|
|
|
455
661
|
return { code: result.output, map: { mappings: '' } };
|
|
456
662
|
}
|
|
457
663
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
664
|
+
// Pre-resolve cross-file type sources for any `component<S, M, E>()`
|
|
665
|
+
// call in this file. The extractors look for `type Msg = ...` etc.
|
|
666
|
+
// in a single source string; if the user keeps `Msg` in a sibling
|
|
667
|
+
// file, the local extraction returns null and the plugin emits no
|
|
668
|
+
// annotations. Pre-resolution chases imports and re-exports to
|
|
669
|
+
// find the declaring file, so the schema/annotation extractors run
|
|
670
|
+
// against the right source. See cross-file-resolver.ts.
|
|
671
|
+
//
|
|
672
|
+
// `this.resolve` may be undefined in test harnesses that call the
|
|
673
|
+
// hook directly without going through Rollup; in that case skip
|
|
674
|
+
// pre-resolution and the local extractors handle whatever's in
|
|
675
|
+
// the source string.
|
|
676
|
+
const resolverAvailable = typeof this.resolve === 'function';
|
|
677
|
+
const [typeSources, preExtracted] = resolverAvailable
|
|
678
|
+
? await Promise.all([
|
|
679
|
+
preResolveTypeSources(code, id, this.resolve.bind(this)),
|
|
680
|
+
// Cross-file + composition-aware extraction. Replaces the
|
|
681
|
+
// file-local sync extractors when active. Without this step
|
|
682
|
+
// a `type Msg = ImportedFoo | { type: 'extra' }`
|
|
683
|
+
// composition would only see the inline `'extra'` variant
|
|
684
|
+
// and silently emit half-annotations.
|
|
685
|
+
preExtractCompositional(code, id, this.resolve.bind(this)),
|
|
686
|
+
])
|
|
687
|
+
: [undefined, undefined];
|
|
688
|
+
const result = transformLlui(code, id, devMode, Boolean(agent), mcpPort, verbose, typeSources, preExtracted);
|
|
480
689
|
if (!result)
|
|
481
690
|
return undefined;
|
|
482
691
|
// Apply per-statement edits via MagicString for accurate source maps.
|