@ggui-ai/dev-stack 0.1.0-rc.1
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/LICENSE +201 -0
- package/README.md +63 -0
- package/dist/dev-server/auth.d.ts +44 -0
- package/dist/dev-server/auth.d.ts.map +1 -0
- package/dist/dev-server/auth.js +176 -0
- package/dist/dev-server/events.d.ts +43 -0
- package/dist/dev-server/events.d.ts.map +1 -0
- package/dist/dev-server/events.js +61 -0
- package/dist/dev-server/http.d.ts +79 -0
- package/dist/dev-server/http.d.ts.map +1 -0
- package/dist/dev-server/http.js +277 -0
- package/dist/dev-server/hub-preview.d.ts +56 -0
- package/dist/dev-server/hub-preview.d.ts.map +1 -0
- package/dist/dev-server/hub-preview.js +237 -0
- package/dist/dev-server/hub.d.ts +59 -0
- package/dist/dev-server/hub.d.ts.map +1 -0
- package/dist/dev-server/hub.js +723 -0
- package/dist/hub-preview/client.js +52346 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/local-registry/compile-ui.d.ts +67 -0
- package/dist/local-registry/compile-ui.d.ts.map +1 -0
- package/dist/local-registry/compile-ui.js +148 -0
- package/dist/local-registry/local-registry.d.ts +93 -0
- package/dist/local-registry/local-registry.d.ts.map +1 -0
- package/dist/local-registry/local-registry.js +272 -0
- package/dist/local-registry/watcher.d.ts +42 -0
- package/dist/local-registry/watcher.d.ts.map +1 -0
- package/dist/local-registry/watcher.js +208 -0
- package/dist/run-dev.d.ts +147 -0
- package/dist/run-dev.d.ts.map +1 -0
- package/dist/run-dev.js +216 -0
- package/dist/runtime-supervisor.d.ts +140 -0
- package/dist/runtime-supervisor.d.ts.map +1 -0
- package/dist/runtime-supervisor.js +91 -0
- package/package.json +72 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@ggui-ai/dev-stack` — shared open dev engine for the ggui
|
|
3
|
+
* protocol. Every CLI / host that runs the local dev loop
|
|
4
|
+
* (`ggui dev` today, future `ggui dev --tunnel`, the local hub,
|
|
5
|
+
* any closed-CLI compat shim) consumes this barrel.
|
|
6
|
+
*
|
|
7
|
+
* Layering:
|
|
8
|
+
*
|
|
9
|
+
* @ggui-ai/ui-registry — contract (types-only)
|
|
10
|
+
* @ggui-ai/project-config — manifest schema / loaders
|
|
11
|
+
* @ggui-ai/agent-runtime — agent runtime adapter seam
|
|
12
|
+
* │
|
|
13
|
+
* ▼
|
|
14
|
+
* @ggui-ai/dev-stack — THIS PACKAGE: local engine
|
|
15
|
+
* │
|
|
16
|
+
* ▼
|
|
17
|
+
* @ggui-ai/cli — `ggui` binary; thin shell
|
|
18
|
+
*
|
|
19
|
+
* Design rule: the agent framework is never hardcoded — one engine
|
|
20
|
+
* composes under every host, with the runtime supplied via an
|
|
21
|
+
* adapter seam.
|
|
22
|
+
*/
|
|
23
|
+
export { LocalUiRegistry, type LocalUiRegistryOptions, type LocalBundleResult, type BundleErrorLocation, } from './local-registry/local-registry.js';
|
|
24
|
+
export { discoverLocalUis, discoverFromGguiJsonPath, type DiscoveredUi, type DiscoveryIssue, type DiscoveryResult, type DiscoverOptions, } from '@ggui-ai/project-config/node';
|
|
25
|
+
export { compileUiOnDemand, resolveEntryFile, COMPILED_BUNDLE_CONTENT_TYPE, type CompileResult, } from './local-registry/compile-ui.js';
|
|
26
|
+
export { createLocalWatcher, statOrNull, type LocalWatcher, type LocalWatcherOptions, type WatchListener, } from './local-registry/watcher.js';
|
|
27
|
+
export { startDevServer, type DevServerOptions, type DevServerHandle, } from './dev-server/http.js';
|
|
28
|
+
export { openEventStream, type SseOptions } from './dev-server/events.js';
|
|
29
|
+
export { renderHubHtml, serveHubShell, type HubShellContext, } from './dev-server/hub.js';
|
|
30
|
+
export { renderHubPreviewHtml, serveHubPreviewShell, serveHubPreviewBundle, resolveHubPreviewBundlePath, extractSelectedId, type HubPreviewShellContext, } from './dev-server/hub-preview.js';
|
|
31
|
+
export { createSecurityPolicy, type CreatePolicyOptions, type DevServerSecurityPolicy, type PolicyOutcome, } from './dev-server/auth.js';
|
|
32
|
+
export { runDev, GguiDevError, DEFAULT_DEV_PORT, DEFAULT_DEV_HOST, type DevOptions, type DevBootstrap, } from './run-dev.js';
|
|
33
|
+
export { RuntimeSupervisor, DEFAULT_RUNTIME_BUFFER_SIZE, emptyRuntimeSnapshot, formatRuntimeEventLine, type RuntimeSupervisorOptions, type RuntimeStateSnapshot, type EmptyRuntimeStateSnapshot, type AnyRuntimeStateSnapshot, type RuntimeEventRecord, } from './runtime-supervisor.js';
|
|
34
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,EACL,eAAe,EACf,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,GACzB,MAAM,oCAAoC,CAAC;AAM5C,OAAO,EACL,gBAAgB,EAChB,wBAAwB,EACxB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,4BAA4B,EAC5B,KAAK,aAAa,GACnB,MAAM,gCAAgC,CAAC;AAGxC,OAAO,EACL,kBAAkB,EAClB,UAAU,EACV,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,aAAa,GACnB,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EACL,cAAc,EACd,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EAAE,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAG1E,OAAO,EACL,aAAa,EACb,aAAa,EACb,KAAK,eAAe,GACrB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,2BAA2B,EAC3B,iBAAiB,EACjB,KAAK,sBAAsB,GAC5B,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EACL,oBAAoB,EACpB,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,KAAK,aAAa,GACnB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,MAAM,EACN,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,UAAU,EACf,KAAK,YAAY,GAClB,MAAM,cAAc,CAAC;AAKtB,OAAO,EACL,iBAAiB,EACjB,2BAA2B,EAC3B,oBAAoB,EACpB,sBAAsB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,GACxB,MAAM,yBAAyB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@ggui-ai/dev-stack` — shared open dev engine for the ggui
|
|
3
|
+
* protocol. Every CLI / host that runs the local dev loop
|
|
4
|
+
* (`ggui dev` today, future `ggui dev --tunnel`, the local hub,
|
|
5
|
+
* any closed-CLI compat shim) consumes this barrel.
|
|
6
|
+
*
|
|
7
|
+
* Layering:
|
|
8
|
+
*
|
|
9
|
+
* @ggui-ai/ui-registry — contract (types-only)
|
|
10
|
+
* @ggui-ai/project-config — manifest schema / loaders
|
|
11
|
+
* @ggui-ai/agent-runtime — agent runtime adapter seam
|
|
12
|
+
* │
|
|
13
|
+
* ▼
|
|
14
|
+
* @ggui-ai/dev-stack — THIS PACKAGE: local engine
|
|
15
|
+
* │
|
|
16
|
+
* ▼
|
|
17
|
+
* @ggui-ai/cli — `ggui` binary; thin shell
|
|
18
|
+
*
|
|
19
|
+
* Design rule: the agent framework is never hardcoded — one engine
|
|
20
|
+
* composes under every host, with the runtime supplied via an
|
|
21
|
+
* adapter seam.
|
|
22
|
+
*/
|
|
23
|
+
// Local UI registry
|
|
24
|
+
export { LocalUiRegistry, } from './local-registry/local-registry.js';
|
|
25
|
+
// Discovery — re-export from @ggui-ai/project-config/node for
|
|
26
|
+
// consumers that have historically imported these symbols from the
|
|
27
|
+
// dev-stack barrel. The canonical home is the schema-owner package;
|
|
28
|
+
// dev-stack's `LocalUiRegistry` consumes the same helper directly.
|
|
29
|
+
export { discoverLocalUis, discoverFromGguiJsonPath, } from '@ggui-ai/project-config/node';
|
|
30
|
+
// Compile-on-demand
|
|
31
|
+
export { compileUiOnDemand, resolveEntryFile, COMPILED_BUNDLE_CONTENT_TYPE, } from './local-registry/compile-ui.js';
|
|
32
|
+
// Watcher
|
|
33
|
+
export { createLocalWatcher, statOrNull, } from './local-registry/watcher.js';
|
|
34
|
+
// HTTP dev server
|
|
35
|
+
export { startDevServer, } from './dev-server/http.js';
|
|
36
|
+
// SSE event stream
|
|
37
|
+
export { openEventStream } from './dev-server/events.js';
|
|
38
|
+
// Local dev hub — HTML shell served from the same server at `/hub`.
|
|
39
|
+
export { renderHubHtml, serveHubShell, } from './dev-server/hub.js';
|
|
40
|
+
// Local dev hub — preview iframe shell served at `/hub/preview`.
|
|
41
|
+
export { renderHubPreviewHtml, serveHubPreviewShell, serveHubPreviewBundle, resolveHubPreviewBundlePath, extractSelectedId, } from './dev-server/hub-preview.js';
|
|
42
|
+
// Auth / CORS policy
|
|
43
|
+
export { createSecurityPolicy, } from './dev-server/auth.js';
|
|
44
|
+
// Orchestration — the seam every local-dev host composes against.
|
|
45
|
+
export { runDev, GguiDevError, DEFAULT_DEV_PORT, DEFAULT_DEV_HOST, } from './run-dev.js';
|
|
46
|
+
// Runtime supervision — ring-buffered view of the supervised agent
|
|
47
|
+
// runtime, consumed by the CLI banner, the future hub, and the
|
|
48
|
+
// HTTP `/runtime/...` snapshot surface.
|
|
49
|
+
export { RuntimeSupervisor, DEFAULT_RUNTIME_BUFFER_SIZE, emptyRuntimeSnapshot, formatRuntimeEventLine, } from './runtime-supervisor.js';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type Message } from 'esbuild';
|
|
2
|
+
import type { UiManifest } from '@ggui-ai/project-config';
|
|
3
|
+
/** Content type the compiled bundle is served with. Matches the
|
|
4
|
+
* precompiled path for consistency. */
|
|
5
|
+
export declare const COMPILED_BUNDLE_CONTENT_TYPE = "application/javascript+react";
|
|
6
|
+
export interface CompileInputs {
|
|
7
|
+
/** Absolute project root — directory containing `ggui.json`. */
|
|
8
|
+
projectRoot: string;
|
|
9
|
+
/** Absolute path to the `ggui.ui.json` this compile is for. */
|
|
10
|
+
manifestPath: string;
|
|
11
|
+
/** Parsed manifest — `entryPoint` drives the compile target when present. */
|
|
12
|
+
manifest: UiManifest;
|
|
13
|
+
}
|
|
14
|
+
export interface CompileOk {
|
|
15
|
+
kind: 'ok';
|
|
16
|
+
/** Raw ESM code. */
|
|
17
|
+
code: string;
|
|
18
|
+
/** Absolute path compiled (the entry point). Useful for logs. */
|
|
19
|
+
entry: string;
|
|
20
|
+
/** esbuild info/warning messages, if any. */
|
|
21
|
+
warnings: Message[];
|
|
22
|
+
}
|
|
23
|
+
export interface CompileMissingEntry {
|
|
24
|
+
kind: 'missing-entry';
|
|
25
|
+
/**
|
|
26
|
+
* Which places were tried — absolute paths. Either the explicit
|
|
27
|
+
* `manifest.entryPoint` (single element) or the fallback filename
|
|
28
|
+
* candidates (in search order).
|
|
29
|
+
*/
|
|
30
|
+
tried: string[];
|
|
31
|
+
}
|
|
32
|
+
export interface CompileFailure {
|
|
33
|
+
kind: 'failure';
|
|
34
|
+
entry: string;
|
|
35
|
+
errors: Message[];
|
|
36
|
+
warnings: Message[];
|
|
37
|
+
}
|
|
38
|
+
export type CompileResult = CompileOk | CompileMissingEntry | CompileFailure;
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the entry file for a UI manifest.
|
|
41
|
+
*
|
|
42
|
+
* Precedence:
|
|
43
|
+
* 1. `manifest.entryPoint` (relative to `projectRoot`, or
|
|
44
|
+
* absolute). Single-candidate result.
|
|
45
|
+
* 2. Fallback: the {@link ENTRY_CANDIDATES} filenames beside the
|
|
46
|
+
* manifest. First hit wins.
|
|
47
|
+
*
|
|
48
|
+
* Returns the resolved absolute path, or `null` with the list of
|
|
49
|
+
* candidates that were tried (for the 404/missing-entry response).
|
|
50
|
+
*/
|
|
51
|
+
export declare function resolveEntryFile(inputs: CompileInputs): {
|
|
52
|
+
entry: string;
|
|
53
|
+
} | {
|
|
54
|
+
tried: string[];
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Compile a UI's TSX source to an ESM bundle. Returns a discriminated
|
|
58
|
+
* result — consumers branch on `kind` to render the right HTTP
|
|
59
|
+
* response / log.
|
|
60
|
+
*
|
|
61
|
+
* esbuild's programmatic build can throw on catastrophic failures
|
|
62
|
+
* (e.g. native binary missing). Those bubble up to the caller — the
|
|
63
|
+
* HTTP layer surfaces them as 500s since they are environment /
|
|
64
|
+
* install problems, not user-source problems.
|
|
65
|
+
*/
|
|
66
|
+
export declare function compileUiOnDemand(inputs: CompileInputs): Promise<CompileResult>;
|
|
67
|
+
//# sourceMappingURL=compile-ui.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compile-ui.d.ts","sourceRoot":"","sources":["../../src/local-registry/compile-ui.ts"],"names":[],"mappings":"AAoCA,OAAO,EAA4B,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AA6B1D;uCACuC;AACvC,eAAO,MAAM,4BAA4B,iCAAiC,CAAC;AAE3E,MAAM,WAAW,aAAa;IAC5B,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,QAAQ,EAAE,UAAU,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,IAAI,CAAC;IACX,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,eAAe,CAAC;IACtB;;;;OAIG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,EAAE,CAAC;IAClB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,mBAAmB,GAAG,cAAc,CAAC;AAE7E;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAkB/F;AAED;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAqCrF"}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-on-demand for local `ggui dev` UIs.
|
|
3
|
+
*
|
|
4
|
+
* Resolves the authored TSX entry for a `ggui.ui.json` and runs
|
|
5
|
+
* esbuild against it, producing an ESM bundle Studio can hand to
|
|
6
|
+
* `DynamicComponent`. This replaces the "must pre-compile" state
|
|
7
|
+
* the foundation slice left behind — `ggui dev` now feels like a
|
|
8
|
+
* real dev server: start it, open Studio, see the render.
|
|
9
|
+
*
|
|
10
|
+
* Trust model:
|
|
11
|
+
*
|
|
12
|
+
* The closed `core/src/validation/ui-compiler.ts` layers in
|
|
13
|
+
* `validateComponentDetailed` — security patterns (`eval`,
|
|
14
|
+
* `fetch`), import allowlist enforcement, size caps. Those exist
|
|
15
|
+
* because the cloud pipeline accepts user-uploaded TSX and runs
|
|
16
|
+
* it downstream. **Localhost `ggui dev` is NOT that pipeline.**
|
|
17
|
+
* The developer is compiling their own project's source on their
|
|
18
|
+
* own machine; a token-gated origin-allowlisted local server has
|
|
19
|
+
* the same trust as the developer's shell. No security
|
|
20
|
+
* validation here.
|
|
21
|
+
*
|
|
22
|
+
* Shape chosen for v1:
|
|
23
|
+
*
|
|
24
|
+
* - esbuild `build` mode with bundle: true, so multi-file UIs
|
|
25
|
+
* work (import sibling helpers, theme tokens, wire hooks).
|
|
26
|
+
* - Sandbox externals match the runtime Studio renderer
|
|
27
|
+
* (`DynamicComponent` in `@ggui-ai/react`). React, the design
|
|
28
|
+
* system, `@ggui-ai/wire`, and `@ggui-ai/react` stay external
|
|
29
|
+
* and resolve against the import map Studio provides at
|
|
30
|
+
* render time.
|
|
31
|
+
* - No caching. Every request recompiles. esbuild TSX transform
|
|
32
|
+
* is ~50-200ms for a typical single-screen UI — fine for the
|
|
33
|
+
* dev loop. A watcher-driven cache lands with HMR, not here.
|
|
34
|
+
*/
|
|
35
|
+
import { existsSync } from 'node:fs';
|
|
36
|
+
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
37
|
+
import { build } from 'esbuild';
|
|
38
|
+
/**
|
|
39
|
+
* Packages the Studio runtime renderer already resolves from its
|
|
40
|
+
* own bundle (via `@ggui-ai/design/module-loader`'s import-map).
|
|
41
|
+
* Keeping them out of the compiled bundle keeps the artifact small
|
|
42
|
+
* and lets Studio's in-tree React instance be shared across every
|
|
43
|
+
* rendered UI.
|
|
44
|
+
*/
|
|
45
|
+
const SANDBOX_EXTERNALS = [
|
|
46
|
+
'react',
|
|
47
|
+
'react/*',
|
|
48
|
+
'react-dom',
|
|
49
|
+
'react-dom/*',
|
|
50
|
+
'@ggui-ai/design',
|
|
51
|
+
'@ggui-ai/design/*',
|
|
52
|
+
'@ggui-ai/wire',
|
|
53
|
+
'@ggui-ai/wire/*',
|
|
54
|
+
'@ggui-ai/react',
|
|
55
|
+
'@ggui-ai/react/*',
|
|
56
|
+
];
|
|
57
|
+
/**
|
|
58
|
+
* Filename candidates for the entry point when the manifest
|
|
59
|
+
* doesn't declare one. Kept narrow so the convention is predictable
|
|
60
|
+
* rather than "anything with a `.tsx` extension."
|
|
61
|
+
*/
|
|
62
|
+
const ENTRY_CANDIDATES = ['ggui.ui.tsx', 'index.tsx', 'component.tsx'];
|
|
63
|
+
/** Content type the compiled bundle is served with. Matches the
|
|
64
|
+
* precompiled path for consistency. */
|
|
65
|
+
export const COMPILED_BUNDLE_CONTENT_TYPE = 'application/javascript+react';
|
|
66
|
+
/**
|
|
67
|
+
* Resolve the entry file for a UI manifest.
|
|
68
|
+
*
|
|
69
|
+
* Precedence:
|
|
70
|
+
* 1. `manifest.entryPoint` (relative to `projectRoot`, or
|
|
71
|
+
* absolute). Single-candidate result.
|
|
72
|
+
* 2. Fallback: the {@link ENTRY_CANDIDATES} filenames beside the
|
|
73
|
+
* manifest. First hit wins.
|
|
74
|
+
*
|
|
75
|
+
* Returns the resolved absolute path, or `null` with the list of
|
|
76
|
+
* candidates that were tried (for the 404/missing-entry response).
|
|
77
|
+
*/
|
|
78
|
+
export function resolveEntryFile(inputs) {
|
|
79
|
+
const { projectRoot, manifestPath, manifest } = inputs;
|
|
80
|
+
if (manifest.entryPoint && manifest.entryPoint.length > 0) {
|
|
81
|
+
const declared = isAbsolute(manifest.entryPoint)
|
|
82
|
+
? manifest.entryPoint
|
|
83
|
+
: resolve(projectRoot, manifest.entryPoint);
|
|
84
|
+
return existsSync(declared) ? { entry: declared } : { tried: [declared] };
|
|
85
|
+
}
|
|
86
|
+
const manifestDir = dirname(manifestPath);
|
|
87
|
+
const tried = [];
|
|
88
|
+
for (const filename of ENTRY_CANDIDATES) {
|
|
89
|
+
const candidate = resolve(manifestDir, filename);
|
|
90
|
+
tried.push(candidate);
|
|
91
|
+
if (existsSync(candidate))
|
|
92
|
+
return { entry: candidate };
|
|
93
|
+
}
|
|
94
|
+
return { tried };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Compile a UI's TSX source to an ESM bundle. Returns a discriminated
|
|
98
|
+
* result — consumers branch on `kind` to render the right HTTP
|
|
99
|
+
* response / log.
|
|
100
|
+
*
|
|
101
|
+
* esbuild's programmatic build can throw on catastrophic failures
|
|
102
|
+
* (e.g. native binary missing). Those bubble up to the caller — the
|
|
103
|
+
* HTTP layer surfaces them as 500s since they are environment /
|
|
104
|
+
* install problems, not user-source problems.
|
|
105
|
+
*/
|
|
106
|
+
export async function compileUiOnDemand(inputs) {
|
|
107
|
+
const resolved = resolveEntryFile(inputs);
|
|
108
|
+
if ('tried' in resolved) {
|
|
109
|
+
return { kind: 'missing-entry', tried: resolved.tried };
|
|
110
|
+
}
|
|
111
|
+
const entry = resolved.entry;
|
|
112
|
+
try {
|
|
113
|
+
const result = await build({
|
|
114
|
+
entryPoints: [entry],
|
|
115
|
+
bundle: true,
|
|
116
|
+
format: 'esm',
|
|
117
|
+
target: 'es2020',
|
|
118
|
+
jsx: 'automatic',
|
|
119
|
+
jsxImportSource: 'react',
|
|
120
|
+
minify: false,
|
|
121
|
+
write: false,
|
|
122
|
+
external: SANDBOX_EXTERNALS,
|
|
123
|
+
platform: 'browser',
|
|
124
|
+
absWorkingDir: inputs.projectRoot,
|
|
125
|
+
treeShaking: true,
|
|
126
|
+
logLevel: 'silent',
|
|
127
|
+
});
|
|
128
|
+
const code = result.outputFiles[0]?.text ?? '';
|
|
129
|
+
return { kind: 'ok', code, entry, warnings: result.warnings };
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
if (isBuildFailure(err)) {
|
|
133
|
+
return {
|
|
134
|
+
kind: 'failure',
|
|
135
|
+
entry,
|
|
136
|
+
errors: err.errors,
|
|
137
|
+
warnings: err.warnings,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function isBuildFailure(err) {
|
|
144
|
+
return (err !== null &&
|
|
145
|
+
typeof err === 'object' &&
|
|
146
|
+
Array.isArray(err.errors) &&
|
|
147
|
+
Array.isArray(err.warnings));
|
|
148
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { UiBundle, UiManifestEntry, UiRegistry, UiRegistryCapabilities } from '@ggui-ai/ui-registry';
|
|
2
|
+
import { type DiscoveryIssue, type GguiJsonV1 } from '@ggui-ai/project-config/node';
|
|
3
|
+
import { type WatchListener } from './watcher.js';
|
|
4
|
+
export interface LocalUiRegistryOptions {
|
|
5
|
+
/** Absolute project root — directory containing `ggui.json`. */
|
|
6
|
+
projectRoot: string;
|
|
7
|
+
/** The parsed `ggui.json`. Only `blueprints.include` is consumed today. */
|
|
8
|
+
manifest: GguiJsonV1;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Richer bundle result — the HTTP layer branches on this to return
|
|
12
|
+
* the right status code / JSON body. `LocalUiRegistry.getBundle`
|
|
13
|
+
* collapses everything into `UiBundle | undefined` per the
|
|
14
|
+
* `@ggui-ai/ui-registry` contract.
|
|
15
|
+
*/
|
|
16
|
+
export type LocalBundleResult = {
|
|
17
|
+
kind: 'ok';
|
|
18
|
+
bundle: UiBundle;
|
|
19
|
+
source: 'precompiled' | 'compiled';
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'not-found';
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'missing-entry';
|
|
24
|
+
tried: string[];
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'compile-failed';
|
|
27
|
+
entry: string;
|
|
28
|
+
errors: Array<{
|
|
29
|
+
text: string;
|
|
30
|
+
location: BundleErrorLocation | null;
|
|
31
|
+
}>;
|
|
32
|
+
warnings: Array<{
|
|
33
|
+
text: string;
|
|
34
|
+
location: BundleErrorLocation | null;
|
|
35
|
+
}>;
|
|
36
|
+
};
|
|
37
|
+
/** Flat, wire-safe subset of esbuild's Location shape. */
|
|
38
|
+
export interface BundleErrorLocation {
|
|
39
|
+
file: string;
|
|
40
|
+
line: number;
|
|
41
|
+
column: number;
|
|
42
|
+
length: number;
|
|
43
|
+
lineText: string;
|
|
44
|
+
}
|
|
45
|
+
export declare class LocalUiRegistry implements UiRegistry {
|
|
46
|
+
readonly capabilities: UiRegistryCapabilities;
|
|
47
|
+
private readonly projectRoot;
|
|
48
|
+
private readonly manifest;
|
|
49
|
+
private entriesById;
|
|
50
|
+
/** Absolute manifest dir → id. Rebuilt on every refresh so the
|
|
51
|
+
* watcher can attribute source-file changes to the right UI. */
|
|
52
|
+
private manifestDirIndex;
|
|
53
|
+
private lastIssues;
|
|
54
|
+
private loaded;
|
|
55
|
+
private watcher;
|
|
56
|
+
constructor(options: LocalUiRegistryOptions);
|
|
57
|
+
/**
|
|
58
|
+
* Force a re-scan of the filesystem against `blueprints.include`.
|
|
59
|
+
* Use after edits when `subscribe` isn't wired yet. The HTTP layer
|
|
60
|
+
* can call this on a PUT `/uis/refresh` or similar — not part of
|
|
61
|
+
* this slice.
|
|
62
|
+
*/
|
|
63
|
+
refresh(): Promise<{
|
|
64
|
+
uiCount: number;
|
|
65
|
+
issues: DiscoveryIssue[];
|
|
66
|
+
}>;
|
|
67
|
+
/**
|
|
68
|
+
* Subscribe to registry change events. Flips
|
|
69
|
+
* `capabilities.observable` from spec to reality. Starts the
|
|
70
|
+
* underlying filesystem watcher on first call and keeps it
|
|
71
|
+
* alive until all listeners unsubscribe AND {@link close} is
|
|
72
|
+
* invoked (lifetime controlled by the enclosing dev server,
|
|
73
|
+
* not by subscriber churn — SSE clients come and go).
|
|
74
|
+
*/
|
|
75
|
+
subscribe(listener: WatchListener): () => void;
|
|
76
|
+
/** Shut down the watcher (if any) and drop listeners. */
|
|
77
|
+
close(): Promise<void>;
|
|
78
|
+
private ensureWatcher;
|
|
79
|
+
/** Diagnostic snapshot — NOT part of the `UiRegistry` surface. */
|
|
80
|
+
getIssues(): readonly DiscoveryIssue[];
|
|
81
|
+
list(): Promise<UiManifestEntry[]>;
|
|
82
|
+
get(id: string): Promise<UiManifestEntry | undefined>;
|
|
83
|
+
getBundle(id: string): Promise<UiBundle | undefined>;
|
|
84
|
+
/**
|
|
85
|
+
* Richer bundle fetch — exposes the distinct "no colocated
|
|
86
|
+
* artifact AND no TSX entry" vs. "compile failed" states that
|
|
87
|
+
* `getBundle` collapses. The HTTP layer uses this directly so
|
|
88
|
+
* failures become actionable 4xx / 422 responses.
|
|
89
|
+
*/
|
|
90
|
+
fetchBundle(id: string): Promise<LocalBundleResult>;
|
|
91
|
+
private ensureLoaded;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=local-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-registry.d.ts","sourceRoot":"","sources":["../../src/local-registry/local-registry.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EACV,QAAQ,EACR,eAAe,EACf,UAAU,EACV,sBAAsB,EACvB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,UAAU,EAChB,MAAM,8BAA8B,CAAC;AAOtC,OAAO,EAAyC,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAQzF,MAAM,WAAW,sBAAsB;IACrC,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,QAAQ,EAAE,UAAU,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,aAAa,GAAG,UAAU,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,GAC1C;IACE,IAAI,EAAE,gBAAgB,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IACtE,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;CACzE,CAAC;AAEN,0DAA0D;AAC1D,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,eAAgB,YAAW,UAAU;IAChD,QAAQ,CAAC,YAAY,EAAE,sBAAsB,CAG3C;IAEF,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAa;IACtC,OAAO,CAAC,WAAW,CAAmC;IACtD;oEACgE;IAChE,OAAO,CAAC,gBAAgB,CAA6B;IACrD,OAAO,CAAC,UAAU,CAAwB;IAC1C,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAA6B;gBAEhC,OAAO,EAAE,sBAAsB;IAK3C;;;;;OAKG;IACG,OAAO,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,cAAc,EAAE,CAAA;KAAE,CAAC;IAkBvE;;;;;;;OAOG;IACH,SAAS,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,IAAI;IAU9C,yDAAyD;IACnD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAO5B,OAAO,CAAC,aAAa;IAYrB,kEAAkE;IAClE,SAAS,IAAI,SAAS,cAAc,EAAE;IAIhC,IAAI,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;IAKlC,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC;IAMrD,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IAK1D;;;;;OAKG;IACG,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;YA0D3C,YAAY;CAK3B"}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `LocalUiRegistry` — the first real implementation of
|
|
3
|
+
* `@ggui-ai/ui-registry`'s `UiRegistry` contract.
|
|
4
|
+
*
|
|
5
|
+
* Scope (compile-on-demand slice, 2026-04-18):
|
|
6
|
+
*
|
|
7
|
+
* - Read-only. `capabilities.writable === false`;
|
|
8
|
+
* `capabilities.observable === false`.
|
|
9
|
+
* - Manifest-backed. Discovery runs against `ggui.json` +
|
|
10
|
+
* `blueprints.include` globs via `./discovery.js` and is
|
|
11
|
+
* cached in memory. `refresh()` re-runs discovery on demand;
|
|
12
|
+
* file watching / `subscribe()` lands with a follow-up
|
|
13
|
+
* HMR slice.
|
|
14
|
+
* - `getBundle(id)` resolution order:
|
|
15
|
+
* 1. **Precompiled fast-path** — colocated `ggui.ui.js` or
|
|
16
|
+
* `ggui.ui.mjs` beside the manifest. Lets a project with
|
|
17
|
+
* its own build step opt out of compile-on-demand.
|
|
18
|
+
* 2. **Compile-on-demand** — resolve the manifest's
|
|
19
|
+
* `entryPoint` (or a conventional `.tsx` filename beside
|
|
20
|
+
* the manifest), run esbuild against it, return the
|
|
21
|
+
* bundled ESM output. See `./compile-ui.ts`.
|
|
22
|
+
* 3. **Undefined** — the id is known to the registry but we
|
|
23
|
+
* could find neither a precompiled artifact nor a TSX
|
|
24
|
+
* entry. Per the `UiRegistry` contract, this is a valid
|
|
25
|
+
* "no bundle" state, not an error — callers render the
|
|
26
|
+
* "no preview available" affordance.
|
|
27
|
+
*
|
|
28
|
+
* Consumers that want to distinguish "missing entry" from "compile
|
|
29
|
+
* failed" need the richer {@link LocalBundleSource.fetchBundle}
|
|
30
|
+
* result — `getBundle` collapses both to the contract's
|
|
31
|
+
* `undefined` so the `UiRegistry` seam stays clean. The HTTP layer
|
|
32
|
+
* calls `fetchBundle` directly for actionable 4xx / 422 responses.
|
|
33
|
+
*/
|
|
34
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
35
|
+
import { dirname, join } from 'node:path';
|
|
36
|
+
import { discoverLocalUis, } from '@ggui-ai/project-config/node';
|
|
37
|
+
import { compileUiOnDemand, COMPILED_BUNDLE_CONTENT_TYPE, resolveEntryFile, } from './compile-ui.js';
|
|
38
|
+
import { createLocalWatcher } from './watcher.js';
|
|
39
|
+
/** Accepted bundle filenames, in preference order. */
|
|
40
|
+
const BUNDLE_FILENAMES = ['ggui.ui.js', 'ggui.ui.mjs'];
|
|
41
|
+
/** Content-Type we claim for the compiled bundle. */
|
|
42
|
+
const BUNDLE_CONTENT_TYPE = 'application/javascript+react';
|
|
43
|
+
export class LocalUiRegistry {
|
|
44
|
+
capabilities = {
|
|
45
|
+
writable: false,
|
|
46
|
+
observable: true,
|
|
47
|
+
};
|
|
48
|
+
projectRoot;
|
|
49
|
+
manifest;
|
|
50
|
+
entriesById = new Map();
|
|
51
|
+
/** Absolute manifest dir → id. Rebuilt on every refresh so the
|
|
52
|
+
* watcher can attribute source-file changes to the right UI. */
|
|
53
|
+
manifestDirIndex = new Map();
|
|
54
|
+
lastIssues = [];
|
|
55
|
+
loaded = false;
|
|
56
|
+
watcher = null;
|
|
57
|
+
constructor(options) {
|
|
58
|
+
this.projectRoot = options.projectRoot;
|
|
59
|
+
this.manifest = options.manifest;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Force a re-scan of the filesystem against `blueprints.include`.
|
|
63
|
+
* Use after edits when `subscribe` isn't wired yet. The HTTP layer
|
|
64
|
+
* can call this on a PUT `/uis/refresh` or similar — not part of
|
|
65
|
+
* this slice.
|
|
66
|
+
*/
|
|
67
|
+
async refresh() {
|
|
68
|
+
const result = await discoverLocalUis({
|
|
69
|
+
projectRoot: this.projectRoot,
|
|
70
|
+
manifest: this.manifest,
|
|
71
|
+
});
|
|
72
|
+
const next = new Map();
|
|
73
|
+
const dirIndex = new Map();
|
|
74
|
+
for (const ui of result.uis) {
|
|
75
|
+
next.set(ui.id, ui);
|
|
76
|
+
dirIndex.set(dirname(ui.manifestPath), ui.id);
|
|
77
|
+
}
|
|
78
|
+
this.entriesById = next;
|
|
79
|
+
this.manifestDirIndex = dirIndex;
|
|
80
|
+
this.lastIssues = result.issues;
|
|
81
|
+
this.loaded = true;
|
|
82
|
+
return { uiCount: result.uis.length, issues: result.issues };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Subscribe to registry change events. Flips
|
|
86
|
+
* `capabilities.observable` from spec to reality. Starts the
|
|
87
|
+
* underlying filesystem watcher on first call and keeps it
|
|
88
|
+
* alive until all listeners unsubscribe AND {@link close} is
|
|
89
|
+
* invoked (lifetime controlled by the enclosing dev server,
|
|
90
|
+
* not by subscriber churn — SSE clients come and go).
|
|
91
|
+
*/
|
|
92
|
+
subscribe(listener) {
|
|
93
|
+
// Ensure the manifest dir index is populated before wiring the
|
|
94
|
+
// watcher — otherwise the first fs events arrive before
|
|
95
|
+
// `refresh()` has ever run and attribute-by-dir lookups return
|
|
96
|
+
// null. `ensureLoaded` is idempotent + cheap.
|
|
97
|
+
void this.ensureLoaded();
|
|
98
|
+
const watcher = this.ensureWatcher();
|
|
99
|
+
return watcher.subscribe(listener);
|
|
100
|
+
}
|
|
101
|
+
/** Shut down the watcher (if any) and drop listeners. */
|
|
102
|
+
async close() {
|
|
103
|
+
if (this.watcher) {
|
|
104
|
+
await this.watcher.close();
|
|
105
|
+
this.watcher = null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
ensureWatcher() {
|
|
109
|
+
if (this.watcher)
|
|
110
|
+
return this.watcher;
|
|
111
|
+
this.watcher = createLocalWatcher({
|
|
112
|
+
projectRoot: this.projectRoot,
|
|
113
|
+
getManifestDirIndex: () => this.manifestDirIndex,
|
|
114
|
+
onManifestsChanged: async () => {
|
|
115
|
+
await this.refresh();
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
return this.watcher;
|
|
119
|
+
}
|
|
120
|
+
/** Diagnostic snapshot — NOT part of the `UiRegistry` surface. */
|
|
121
|
+
getIssues() {
|
|
122
|
+
return this.lastIssues;
|
|
123
|
+
}
|
|
124
|
+
async list() {
|
|
125
|
+
await this.ensureLoaded();
|
|
126
|
+
return Array.from(this.entriesById.values()).map(toEntry);
|
|
127
|
+
}
|
|
128
|
+
async get(id) {
|
|
129
|
+
await this.ensureLoaded();
|
|
130
|
+
const ui = this.entriesById.get(id);
|
|
131
|
+
return ui ? toEntry(ui) : undefined;
|
|
132
|
+
}
|
|
133
|
+
async getBundle(id) {
|
|
134
|
+
const result = await this.fetchBundle(id);
|
|
135
|
+
return result.kind === 'ok' ? result.bundle : undefined;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Richer bundle fetch — exposes the distinct "no colocated
|
|
139
|
+
* artifact AND no TSX entry" vs. "compile failed" states that
|
|
140
|
+
* `getBundle` collapses. The HTTP layer uses this directly so
|
|
141
|
+
* failures become actionable 4xx / 422 responses.
|
|
142
|
+
*/
|
|
143
|
+
async fetchBundle(id) {
|
|
144
|
+
await this.ensureLoaded();
|
|
145
|
+
const ui = this.entriesById.get(id);
|
|
146
|
+
if (!ui)
|
|
147
|
+
return { kind: 'not-found' };
|
|
148
|
+
// Freshness rule (2026-04-18):
|
|
149
|
+
// precompiled `.js` serves only if it's AT LEAST AS NEW as
|
|
150
|
+
// the resolved TSX entry. If the TSX is newer — the user just
|
|
151
|
+
// edited it — we recompile and ignore the stale precompiled
|
|
152
|
+
// artifact. Stateless, watcher-independent, always correct.
|
|
153
|
+
//
|
|
154
|
+
// Without this rule a developer's save of ggui.ui.tsx would be
|
|
155
|
+
// silently masked by an older ggui.ui.js sitting beside it —
|
|
156
|
+
// exactly the "edit TSX, still serves stale JS" failure mode
|
|
157
|
+
// the HMR slice must not leave behind.
|
|
158
|
+
const precompiledPath = findColocatedBundlePath(ui.manifestPath);
|
|
159
|
+
const entryResolution = resolveEntryFile({
|
|
160
|
+
projectRoot: this.projectRoot,
|
|
161
|
+
manifestPath: ui.manifestPath,
|
|
162
|
+
manifest: ui.manifest,
|
|
163
|
+
});
|
|
164
|
+
const entryPath = 'entry' in entryResolution ? entryResolution.entry : null;
|
|
165
|
+
if (precompiledPath && shouldPreferPrecompiled(precompiledPath, entryPath)) {
|
|
166
|
+
const code = readFileSync(precompiledPath, 'utf-8');
|
|
167
|
+
return {
|
|
168
|
+
kind: 'ok',
|
|
169
|
+
bundle: { code, contentType: BUNDLE_CONTENT_TYPE },
|
|
170
|
+
source: 'precompiled',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// Compile-on-demand from the authored TSX entry (or surface
|
|
174
|
+
// missing-entry if neither precompiled nor entry exists).
|
|
175
|
+
const compiled = await compileUiOnDemand({
|
|
176
|
+
projectRoot: this.projectRoot,
|
|
177
|
+
manifestPath: ui.manifestPath,
|
|
178
|
+
manifest: ui.manifest,
|
|
179
|
+
});
|
|
180
|
+
if (compiled.kind === 'ok') {
|
|
181
|
+
return {
|
|
182
|
+
kind: 'ok',
|
|
183
|
+
bundle: { code: compiled.code, contentType: COMPILED_BUNDLE_CONTENT_TYPE },
|
|
184
|
+
source: 'compiled',
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (compiled.kind === 'missing-entry') {
|
|
188
|
+
return { kind: 'missing-entry', tried: compiled.tried };
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
kind: 'compile-failed',
|
|
192
|
+
entry: compiled.entry,
|
|
193
|
+
errors: compiled.errors.map(toBundleMessage),
|
|
194
|
+
warnings: compiled.warnings.map(toBundleMessage),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
async ensureLoaded() {
|
|
198
|
+
if (!this.loaded) {
|
|
199
|
+
await this.refresh();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function toBundleMessage(msg) {
|
|
204
|
+
const location = msg.location
|
|
205
|
+
? {
|
|
206
|
+
file: msg.location.file,
|
|
207
|
+
line: msg.location.line,
|
|
208
|
+
column: msg.location.column,
|
|
209
|
+
length: msg.location.length,
|
|
210
|
+
lineText: msg.location.lineText,
|
|
211
|
+
}
|
|
212
|
+
: null;
|
|
213
|
+
return { text: msg.text, location };
|
|
214
|
+
}
|
|
215
|
+
function toEntry(ui) {
|
|
216
|
+
// `manifest.contentHash` is optional per the schema — authored
|
|
217
|
+
// manifests typically omit it until a compile step fills it in.
|
|
218
|
+
// The registry contract requires a non-undefined `contentHash`
|
|
219
|
+
// string; empty signals "no artifact-version pin yet", which is
|
|
220
|
+
// correct for a dev-only source registry.
|
|
221
|
+
return {
|
|
222
|
+
id: ui.id,
|
|
223
|
+
contentHash: ui.manifest.contentHash ?? '',
|
|
224
|
+
manifest: ui.manifest,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Locate the colocated precompiled bundle next to a manifest,
|
|
229
|
+
* if any. Returns the first matching filename per
|
|
230
|
+
* {@link BUNDLE_FILENAMES} preference or null. Does NOT read the
|
|
231
|
+
* bytes — the freshness comparison runs first, and we only
|
|
232
|
+
* actually read the file when we've decided to serve it.
|
|
233
|
+
*/
|
|
234
|
+
function findColocatedBundlePath(manifestPath) {
|
|
235
|
+
const dir = dirname(manifestPath);
|
|
236
|
+
for (const filename of BUNDLE_FILENAMES) {
|
|
237
|
+
const candidate = join(dir, filename);
|
|
238
|
+
if (existsSync(candidate))
|
|
239
|
+
return candidate;
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Mtime-based freshness gate between a precompiled artifact and
|
|
245
|
+
* an authored TSX entry.
|
|
246
|
+
*
|
|
247
|
+
* - No TSX entry at all → precompiled wins (nothing newer could
|
|
248
|
+
* replace it; this is the pre-compile-on-demand case).
|
|
249
|
+
* - TSX exists and is newer than precompiled → recompile. The
|
|
250
|
+
* developer just edited; serving stale JS is the UX bug this
|
|
251
|
+
* rule exists to prevent.
|
|
252
|
+
* - Precompiled newer or equal → precompiled wins (user's own
|
|
253
|
+
* build step ran; honour it).
|
|
254
|
+
*
|
|
255
|
+
* mtime-compare is stateless, watcher-independent, and correct
|
|
256
|
+
* even when subscribers are not connected.
|
|
257
|
+
*/
|
|
258
|
+
function shouldPreferPrecompiled(precompiledPath, entryPath) {
|
|
259
|
+
if (!entryPath)
|
|
260
|
+
return true;
|
|
261
|
+
try {
|
|
262
|
+
const pre = statSync(precompiledPath).mtimeMs;
|
|
263
|
+
const src = statSync(entryPath).mtimeMs;
|
|
264
|
+
return pre >= src;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// If a stat fails mid-check the safer call is to fall through
|
|
268
|
+
// to recompile — a compile-on-demand with a live entry file
|
|
269
|
+
// always produces fresh output.
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|