Skip to content

Commit 42a61cc

Browse files
committed
Reduce bridge artifact drift across the app-builder workflow
Fulora's app-facing bridge story had split across generator output, templates, samples, and CLI workflows. This change moves the common path toward generated service facades, makes bridge artifact generation explicit and complete, and teaches `dev` and `package` to validate manifest-based consistency before developers fall into stale-contract failures. The CLI now emits and consumes a bridge artifact manifest, exposes `--preflight-only` for fast checks, and uses more stable build invocation defaults for nested CLI-driven bridge builds in tests and real workflows. Constraint: Must preserve the existing bridge transport/runtime substrate while improving app-builder DX Constraint: Must keep legacy service exports available during the transition to `services.*`-style facades Rejected: Auto-fix missing/stale artifacts inside `dev`/`package` | user explicitly requested manifest/hash consistency rather than implicit mutation Rejected: Keep timestamp-only freshness checks | too noisy and not authoritative enough for build/TFM drift Confidence: medium Scope-risk: moderate Reversibility: clean Directive: Keep bridge artifact consistency logic centralized; do not re-spread manifest/hash checks across commands without a shared helper Directive: If manifest schema changes, update both CLI generation and MSBuild target emission in lockstep Tested: `dotnet test tests/Agibuild.Fulora.UnitTests/Agibuild.Fulora.UnitTests.csproj -v minimal` (2177 passed) Tested: Template/sample web builds for React, Vue, Showcase Todo, and AI Chat during implementation Not-tested: End-to-end desktop runtime behavior after `dev`/`package` preflight warnings on a freshly scaffolded app
1 parent 5038692 commit 42a61cc

35 files changed

Lines changed: 1704 additions & 125 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,9 @@ webView.Bridge.Expose<IGreeterService>(new GreeterService());
136136
Call from JavaScript:
137137

138138
```javascript
139-
const msg = await window.agWebView.rpc.invoke("GreeterService.greet", {
140-
name: "World"
141-
});
139+
import { services } from "./bridge/client";
140+
141+
const msg = await services.greeter.greet({ name: "World" });
142142
```
143143

144144
Call JavaScript from C#:

docs/articles/bridge-guide.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ webView.Bridge.Expose<ICalculatorService>(new CalculatorService());
4545
### 2) Call from JavaScript
4646

4747
```javascript
48-
const sum = await window.agWebView.rpc.invoke("Calculator.add", { a: 3, b: 4 });
48+
import { services } from "./bridge/client";
49+
50+
const sum = await services.calculator.add({ a: 3, b: 4 });
4951
```
5052

5153
### 3) Call JavaScript service from C# (`[JsImport]`)

docs/articles/getting-started.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ WebView.Bridge.Expose<IGreeterService>(new GreeterServiceImpl());
153153
Call it from JavaScript — the bridge client is auto-generated:
154154

155155
```javascript
156-
const result = await GreeterService.greet("World");
156+
import { services } from "./bridge/client";
157+
158+
const result = await services.greeter.greet({ name: "World" });
157159
// → "Hello, World!"
158160
```
159161

docs/cli.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,23 +33,28 @@ Start the Vite dev server and Avalonia desktop app together. Run from the soluti
3333
```bash
3434
fulora dev
3535
fulora dev --web ./MyApp.Web.Vite.React --desktop ./MyApp.Desktop/MyApp.Desktop.csproj
36+
fulora dev --preflight-only
3637
```
3738

3839
| Option | Description |
3940
|---|---|
4041
| `--web` | Web project directory (auto-detected) |
4142
| `--desktop` | Desktop `.csproj` path (auto-detected) |
4243
| `--npm-script` | npm script name (default: `dev`) |
44+
| `--preflight-only` | Run bridge/dev preflight checks and exit without starting the dev processes |
4345

4446
Press **Ctrl+C** to stop both processes.
4547

48+
Use `--preflight-only` when you want to validate bridge artifact consistency and project wiring without launching Vite or the desktop host.
49+
4650
### `fulora package`
4751

4852
Package your app for distribution. The recommended first path is to start with a named profile.
4953

5054
```bash
5155
fulora package --project ./src/MyApp.Desktop/MyApp.Desktop.csproj --profile desktop-public
5256
fulora package --project ./src/MyApp.Desktop/MyApp.Desktop.csproj --profile mac-notarized
57+
fulora package --project ./src/MyApp.Desktop/MyApp.Desktop.csproj --profile desktop-public --preflight-only
5358
```
5459

5560
Available profiles today:
@@ -69,16 +74,30 @@ Available profiles today:
6974
| `--sign-params`, `-n` | Raw signing parameters passed to `vpk` |
7075
| `--notarize` | Enable macOS notarization |
7176
| `--channel`, `-c` | Release channel |
77+
| `--preflight-only` | Run packaging and bridge consistency preflight checks, then exit without publishing or packing |
7278

7379
If `vpk` is not installed, `fulora package` falls back to copying the `dotnet publish` output into the output directory.
7480

81+
The command now emits **preflight notes** before packaging when the selected profile implies extra setup. Examples:
82+
83+
- `desktop-public` without `vpk` → warns that Fulora will copy publish output instead of producing installer/update packages
84+
- `mac-notarized` without `vpk` → warns that the fallback output will not be notarized
85+
- `mac-notarized` on a non-macOS host → warns that notarization usually needs a macOS host
86+
87+
Use `--preflight-only` when you want those checks without triggering `dotnet publish` or `vpk`.
88+
7589
## Advanced Workflows
7690

7791
Use these commands after the main path is already working.
7892

7993
### `fulora generate types`
8094

81-
Build the Bridge project and extract generated TypeScript declarations.
95+
Build the Bridge project and extract the generated bridge artifacts:
96+
97+
- `bridge.d.ts`
98+
- `bridge.client.ts`
99+
- `bridge.mock.ts`
100+
- `bridge.manifest.json`
82101

83102
```bash
84103
fulora generate types
@@ -88,7 +107,9 @@ fulora gen types --project ./MyApp.Bridge/MyApp.Bridge.csproj --output ./MyApp.W
88107
| Option | Description |
89108
|---|---|
90109
| `--project`, `-p` | Bridge `.csproj` path (auto-detected) |
91-
| `--output`, `-o` | Output directory for generated `.d.ts` files (auto-detected) |
110+
| `--output`, `-o` | Output directory for generated bridge artifacts (auto-detected; prefers `src/bridge/generated` when present) |
111+
112+
The generated `bridge.manifest.json` records the bridge project identity, artifact directory, build configuration, target framework, bridge assembly hash, and artifact hashes so `fulora dev` and `fulora package` can detect missing or stale generated outputs without relying only on timestamps.
92113

93114
### `fulora add service <name>`
94115

docs/shipping-your-app.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ fulora package --project ./src/MyApp.Desktop/MyApp.Desktop.csproj \
1818
--profile desktop-public \
1919
--version 1.0.0 \
2020
--output ./Releases
21+
22+
fulora package --project ./src/MyApp.Desktop/MyApp.Desktop.csproj \
23+
--profile desktop-public \
24+
--preflight-only
2125
```
2226

2327
Profiles are the productized shipping path in Fulora. They bundle the recommended defaults for a release scenario so you do not need to remember low-level flags every time.
@@ -30,6 +34,8 @@ Current built-in profiles:
3034

3135
You can still override profile defaults with explicit flags. For example, `--runtime linux-x64` or `--channel preview` wins over the profile setting.
3236

37+
If you want to verify packaging prerequisites without running publish/pack yet, use `--preflight-only`.
38+
3339
### 2. Package command options
3440

3541
| Option | Description | Default |
@@ -52,6 +58,14 @@ You can still override profile defaults with explicit flags. For example, `--run
5258

5359
If `vpk` is not installed, `fulora package` falls back to copying the `dotnet publish` output into the output directory.
5460

61+
Fulora now prints **preflight notes** before packaging when the chosen profile implies extra setup. Typical examples:
62+
63+
- `desktop-public` without `vpk`: you will get copied publish output, not installer/update packages
64+
- `mac-notarized` without `vpk`: the fallback output will not be notarized
65+
- `mac-notarized` on a non-macOS host: you may need to finish signing/notarization on macOS
66+
67+
`fulora package` also checks the bridge artifact manifest (`bridge.manifest.json`) when it can locate a sibling Bridge project. That lets it warn about missing or stale generated bridge files before packaging continues.
68+
5569
## Raw Signing And Notarization Flags
5670

5771
Most teams should stay on the profile-based path. Use the raw flags below when you need to tune signing behavior for a specific environment.
Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,43 @@
1-
import { createBridgeProfile } from '@agibuild/bridge/profile';
1+
import {
2+
createBridgeClient,
3+
type BridgeReadyOptions,
4+
withErrorNormalization,
5+
withLogging,
6+
} from '@agibuild/bridge';
7+
import { aiChatService, windowShellBridgeService } from './generated/bridge.client';
28

3-
export const bridgeProfile = createBridgeProfile({
4-
enableLogging: import.meta.env.DEV,
5-
logging: { maxParamLength: 200 },
6-
});
9+
export type {
10+
AiModelState,
11+
DroppedFileResult,
12+
TransparencyLevel,
13+
WindowShellState,
14+
WindowShellSettings,
15+
} from './generated/bridge.d';
716

8-
export const bridge = bridgeProfile.bridge;
17+
const bridgeClient = createBridgeClient();
18+
19+
if (import.meta.env.DEV) {
20+
bridgeClient.use(withLogging({ maxParamLength: 200 }));
21+
}
22+
23+
bridgeClient.use(withErrorNormalization());
24+
25+
export const bridge = bridgeClient;
26+
27+
export const bridgeProfile = {
28+
bridge,
29+
ready(options?: BridgeReadyOptions) {
30+
return bridge.ready(options);
31+
},
32+
};
33+
34+
export function createFuloraClient() {
35+
return {
36+
aiChat: aiChatService,
37+
windowShellBridge: windowShellBridgeService,
38+
} as const;
39+
}
40+
41+
export const services = createFuloraClient();
42+
43+
export { aiChatService, windowShellBridgeService };
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
export { aiChatService, windowShellBridgeService } from './generated/bridge.client';
1+
export { aiChatService, createFuloraClient, services, windowShellBridgeService } from './client';
22

33
export type {
44
AiModelState,
55
DroppedFileResult,
66
TransparencyLevel,
77
WindowShellState,
88
WindowShellSettings,
9-
} from './generated/bridge.d';
9+
} from './client';

samples/avalonia-react/AvaloniReact.Web/src/bridge/client.ts

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,65 @@
33
* Configures cross-cutting concerns (logging, error normalization) before any service calls.
44
*/
55

6-
import { createBridgeProfile } from '@agibuild/bridge/profile';
6+
import {
7+
createBridgeClient,
8+
type BridgeReadyOptions,
9+
withErrorNormalization,
10+
withLogging,
11+
} from '@agibuild/bridge';
12+
import {
13+
appShellService,
14+
chatService,
15+
fileService,
16+
settingsService,
17+
systemInfoService,
18+
} from './generated/bridge.client';
719

8-
export const bridgeProfile = createBridgeProfile({
9-
enableLogging: import.meta.env.DEV,
10-
logging: { maxParamLength: 100 },
11-
});
20+
export type {
21+
AppInfo,
22+
AppSettings,
23+
ChatMessage,
24+
ChatRequest,
25+
ChatResponse,
26+
FileEntry,
27+
PageDefinition,
28+
RuntimeMetrics,
29+
SystemInfo,
30+
} from './generated/bridge.d';
1231

13-
export const bridge = bridgeProfile.bridge;
32+
const bridgeClient = createBridgeClient();
33+
34+
if (import.meta.env.DEV) {
35+
bridgeClient.use(withLogging({ maxParamLength: 100 }));
36+
}
37+
38+
bridgeClient.use(withErrorNormalization());
39+
40+
export const bridge = bridgeClient;
41+
42+
export const bridgeProfile = {
43+
bridge,
44+
ready(options?: BridgeReadyOptions) {
45+
return bridge.ready(options);
46+
},
47+
};
48+
49+
export function createFuloraClient() {
50+
return {
51+
appShell: appShellService,
52+
chat: chatService,
53+
file: fileService,
54+
settings: settingsService,
55+
systemInfo: systemInfoService,
56+
} as const;
57+
}
58+
59+
export const services = createFuloraClient();
60+
61+
export {
62+
appShellService,
63+
chatService,
64+
fileService,
65+
settingsService,
66+
systemInfoService,
67+
};

samples/avalonia-react/AvaloniReact.Web/src/bridge/services.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
*/
55

66
export {
7+
createFuloraClient,
8+
services,
79
appShellService,
810
systemInfoService,
911
chatService,
1012
fileService,
1113
settingsService,
12-
} from './generated/bridge.client';
14+
} from './client';
1315

1416
export type {
1517
AppInfo,
@@ -21,4 +23,4 @@ export type {
2123
PageDefinition,
2224
RuntimeMetrics,
2325
SystemInfo,
24-
} from './generated/bridge.d';
26+
} from './client';
Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,47 @@
1-
import { createBridgeProfile } from '@agibuild/bridge/profile';
1+
import { BridgeReadyTimeoutError, createBridgeClient, withErrorNormalization, withLogging, } from '@agibuild/bridge';
2+
import { appShellService, chatService, fileService, settingsService, systemInfoService, } from './generated/bridge.client';
23
import { installBridgeMock } from './generated/bridge.mock';
3-
export const bridgeProfile = createBridgeProfile({
4-
enableLogging: import.meta.env.DEV,
5-
logging: { maxParamLength: 200 },
6-
installMock: installBridgeMock,
7-
});
8-
export const bridge = bridgeProfile.bridge;
4+
const bridgeClient = createBridgeClient();
5+
if (import.meta.env.DEV) {
6+
bridgeClient.use(withLogging({ maxParamLength: 200 }));
7+
}
8+
bridgeClient.use(withErrorNormalization());
9+
let mockInstalled = false;
10+
function installMockOnce() {
11+
if (mockInstalled) {
12+
return;
13+
}
14+
installBridgeMock();
15+
mockInstalled = true;
16+
}
17+
export const bridge = bridgeClient;
18+
export const bridgeProfile = {
19+
bridge,
20+
get isMockMode() {
21+
return mockInstalled;
22+
},
23+
async ready(options) {
24+
try {
25+
await bridge.ready(options);
26+
}
27+
catch (err) {
28+
if (!mockInstalled && err instanceof BridgeReadyTimeoutError) {
29+
installMockOnce();
30+
await bridge.ready(options);
31+
return;
32+
}
33+
throw err;
34+
}
35+
},
36+
};
37+
export function createFuloraClient() {
38+
return {
39+
appShell: appShellService,
40+
chat: chatService,
41+
file: fileService,
42+
settings: settingsService,
43+
systemInfo: systemInfoService,
44+
};
45+
}
46+
export const services = createFuloraClient();
47+
export { appShellService, chatService, fileService, settingsService, systemInfoService, };

0 commit comments

Comments
 (0)