@async/framework 0.11.17 → 0.11.19

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/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.19 - 2026-06-19
4
+
5
+ - Added the first build-required profile subpaths,
6
+ `@async/framework/jsx` and `@async/framework/vite`, with package export,
7
+ declaration, pack, and installed-package coverage.
8
+ - Added JSX authoring markers for `signal`, `component`, `Suspense`, and
9
+ `Reveal` that stay inert for compiler analysis instead of executing app code.
10
+ - Added a Vite 8+ Rolldown plugin spike that emits a deterministic virtual
11
+ runtime plan/report from fixture metadata and rejects unsupported hosts before
12
+ transform output is trusted.
13
+ - Added the build-profile report fixture proving selected runtime slices,
14
+ omitted no-build systems, visible fallbacks, signal/event/stream counts, and
15
+ generated locator counts without importing the root `Async` app hub.
16
+ - Bundle size from bundled TypeScript source: `browser.ts` raw 221,403 B (221.4 KB / 0.221 MB), gzip 42,093 B (42.1 KB / 0.042 MB), br 34,797 B (34.8 KB / 0.035 MB) -> `browser.min.js` raw 95,027 B (95.0 KB / 0.095 MB), gzip 28,145 B (28.1 KB / 0.028 MB), br 24,793 B (24.8 KB / 0.025 MB); delta raw -126,376 B (-126.4 KB / -0.126 MB), gzip -13,948 B (-13.9 KB / -0.014 MB), br -10,004 B (-10.0 KB / -0.010 MB).
17
+
18
+ ## 0.11.18 - 2026-06-19
19
+
20
+ - Added inert build optimizer artifact helpers for ADR 26 pass records,
21
+ diagnostics, runtime slice selection, handler emission, and development
22
+ report generation.
23
+ - Added optimizer fixtures for signal source classification, signal ownership,
24
+ JSX event symbol extraction, Suspense/Reveal lowering, runtime selection, and
25
+ server-only browser import diagnostics.
26
+ - Added build optimizer tests proving maybe-promise signals fail explicitly,
27
+ event handlers are not forced through dynamic imports, Reveal ordering is
28
+ deterministic, omitted runtime systems are visible, and helpers do not execute
29
+ app modules or import no-build runtime systems.
30
+ - Bundle size from bundled TypeScript source: `browser.ts` raw 221,403 B (221.4 KB / 0.221 MB), gzip 42,093 B (42.1 KB / 0.042 MB), br 34,797 B (34.8 KB / 0.035 MB) -> `browser.min.js` raw 95,027 B (95.0 KB / 0.095 MB), gzip 28,145 B (28.1 KB / 0.028 MB), br 24,793 B (24.8 KB / 0.025 MB); delta raw -126,376 B (-126.4 KB / -0.126 MB), gzip -13,948 B (-13.9 KB / -0.014 MB), br -10,004 B (-10.0 KB / -0.010 MB).
31
+
3
32
  ## 0.11.17 - 2026-06-19
4
33
 
5
34
  - Added the stream backpatch protocol to `createBoundaryReceiver(...)` with
@@ -0,0 +1,736 @@
1
+ export const OPTIMIZER_ARTIFACT_VERSION = 1;
2
+
3
+ export const OPTIMIZER_PASSES = Object.freeze([
4
+ "source-inventory",
5
+ "jsx-semantic-graph",
6
+ "signal-source-classification",
7
+ "signal-ownership-lifetime",
8
+ "event-symbol-extraction",
9
+ "suspense-reveal-lowering",
10
+ "runtime-slice-selection",
11
+ "handler-emission",
12
+ "plan-bootstrap-emit"
13
+ ]);
14
+
15
+ export const RUNTIME_ENTRYPOINTS = Object.freeze({
16
+ runtime: "@async/framework/runtime",
17
+ signals: "@async/framework/runtime/signals",
18
+ events: "@async/framework/runtime/events"
19
+ });
20
+
21
+ export const RUNTIME_SLICE_NAMES = Object.freeze([
22
+ "signals",
23
+ "events",
24
+ "async-signals",
25
+ "stream"
26
+ ]);
27
+
28
+ const omittedRuntimeSystems = Object.freeze([
29
+ "no-build-loader",
30
+ "router",
31
+ "server",
32
+ "cache",
33
+ "partials",
34
+ "components",
35
+ "boundary-receiver"
36
+ ]);
37
+
38
+ const validRevealOrders = new Set(["as-ready", "forwards", "backwards", "together"]);
39
+ const validRevealTails = new Set(["visible", "collapsed", "hidden"]);
40
+ const validHandlerModes = new Set(["inline", "direct-import", "eager-chunk", "lazy-chunk"]);
41
+
42
+ export function createOptimizerArtifactSet(input = {}) {
43
+ const diagnostics = [];
44
+ const sourceInventory = createSourceInventoryArtifact(input.sourceInventory, { diagnostics });
45
+ const jsxSemanticGraph = createJsxSemanticGraphArtifact(input.semanticGraph, { diagnostics });
46
+ const signalSources = classifySignalSources(jsxSemanticGraph.signals, { diagnostics }).artifact;
47
+ const signalOwnership = inferSignalOwnership(jsxSemanticGraph.signals, { diagnostics }).artifact;
48
+ const eventSymbols = extractEventSymbols(jsxSemanticGraph.eventProps, { diagnostics }).artifact;
49
+ const streamBoundaries = lowerSuspenseReveal(jsxSemanticGraph, { diagnostics }).artifact;
50
+ const runtimeSelection = selectRuntimeSlices({
51
+ sourceInventory,
52
+ signalSources,
53
+ eventSymbols,
54
+ streamBoundaries
55
+ }, { diagnostics }).artifact;
56
+ const handlerEmission = planHandlerEmission(eventSymbols.handlers, { diagnostics }).artifact;
57
+ const report = createOptimizerReport({
58
+ sourceInventory,
59
+ signalSources,
60
+ signalOwnership,
61
+ eventSymbols,
62
+ streamBoundaries,
63
+ runtimeSelection,
64
+ handlerEmission,
65
+ jsxSemanticGraph
66
+ });
67
+ const buildEmit = {
68
+ bootstrap: {
69
+ mode: "fixture",
70
+ entrypoint: runtimeSelection.entrypoint,
71
+ slices: runtimeSelection.slices.map((slice) => slice.name)
72
+ },
73
+ manifest: {
74
+ version: OPTIMIZER_ARTIFACT_VERSION,
75
+ passes: OPTIMIZER_PASSES
76
+ },
77
+ report
78
+ };
79
+
80
+ return {
81
+ version: OPTIMIZER_ARTIFACT_VERSION,
82
+ passes: OPTIMIZER_PASSES,
83
+ artifacts: {
84
+ sourceInventory,
85
+ jsxSemanticGraph,
86
+ signalSources,
87
+ signalOwnership,
88
+ eventSymbols,
89
+ streamBoundaries,
90
+ runtimeSelection,
91
+ handlerEmission,
92
+ buildEmit
93
+ },
94
+ diagnostics,
95
+ report
96
+ };
97
+ }
98
+
99
+ export function createSourceInventoryArtifact(input = {}, options = {}) {
100
+ const diagnostics = getDiagnostics(options);
101
+ const config = input.config ?? {};
102
+ const host = config.host ?? {};
103
+ const hostName = host.name ?? config.hostName;
104
+ const hostVersion = Number(host.major ?? host.versionMajor ?? parseVersionMajor(host.version));
105
+ const hostEngine = host.engine ?? config.engine;
106
+
107
+ if (hostName && hostName !== "vite") {
108
+ addDiagnostic(diagnostics, "unsupported-build-host", "Only Vite 8+ with Rolldown is supported by the build optimizer.", {
109
+ pass: "source-inventory",
110
+ value: hostName
111
+ });
112
+ }
113
+ if (hostName === "vite" && hostVersion && hostVersion < 8) {
114
+ addDiagnostic(diagnostics, "unsupported-build-host", "Vite hosts must be version 8 or newer.", {
115
+ pass: "source-inventory",
116
+ value: host.version ?? hostVersion
117
+ });
118
+ }
119
+ if (hostName === "vite" && hostEngine && hostEngine !== "rolldown") {
120
+ addDiagnostic(diagnostics, "unsupported-build-host", "The initial build optimizer supports the Rolldown engine only.", {
121
+ pass: "source-inventory",
122
+ value: hostEngine
123
+ });
124
+ }
125
+
126
+ const frameworkImports = arrayOf(input.frameworkImports);
127
+ for (const frameworkImport of frameworkImports) {
128
+ if (frameworkImport.supported === false) {
129
+ addDiagnostic(diagnostics, "unsupported-framework-import-shape", "Framework import shape is not supported by the build optimizer.", {
130
+ pass: "source-inventory",
131
+ sourceId: frameworkImport.sourceId ?? frameworkImport.module
132
+ });
133
+ }
134
+ }
135
+
136
+ const serverModules = arrayOf(input.serverModules);
137
+ const serverIds = new Set(serverModules.map((module) => module.id).filter(Boolean));
138
+ const entries = arrayOf(input.entries);
139
+ for (const entry of entries) {
140
+ if (entry.target !== "browser") {
141
+ continue;
142
+ }
143
+ for (const imported of arrayOf(entry.imports)) {
144
+ if (serverIds.has(imported)) {
145
+ addDiagnostic(diagnostics, "browser-imports-server-only-code", "Browser output cannot import server-only modules.", {
146
+ pass: "source-inventory",
147
+ sourceId: entry.id,
148
+ value: imported
149
+ });
150
+ }
151
+ }
152
+ }
153
+
154
+ return {
155
+ version: OPTIMIZER_ARTIFACT_VERSION,
156
+ entries,
157
+ frameworkImports,
158
+ jsxModules: arrayOf(input.jsxModules),
159
+ serverModules,
160
+ config
161
+ };
162
+ }
163
+
164
+ export function createJsxSemanticGraphArtifact(input = {}) {
165
+ return {
166
+ version: OPTIMIZER_ARTIFACT_VERSION,
167
+ components: arrayOf(input.components),
168
+ signals: arrayOf(input.signals),
169
+ eventProps: arrayOf(input.eventProps),
170
+ suspense: arrayOf(input.suspense),
171
+ revealPolicies: arrayOf(input.revealPolicies),
172
+ serverCalls: arrayOf(input.serverCalls),
173
+ routes: arrayOf(input.routes),
174
+ locators: arrayOf(input.locators)
175
+ };
176
+ }
177
+
178
+ export function classifySignalSources(signalFeatures = [], options = {}) {
179
+ const diagnostics = getDiagnostics(options);
180
+ const sources = [];
181
+
182
+ for (const feature of arrayOf(signalFeatures)) {
183
+ const sourceId = requireFeatureId(feature, "signal", diagnostics, "signal-source-classification");
184
+ if (!sourceId) {
185
+ continue;
186
+ }
187
+ const sourceShape = feature.sourceShape ?? feature.shape ?? feature.sourceKind;
188
+ const dependencies = normalizeReads(feature.reads ?? feature.dependencies);
189
+
190
+ if (sourceShape === "value" || sourceShape === "literal" || sourceShape === "non-function") {
191
+ sources.push({
192
+ kind: "writable",
193
+ sourceId,
194
+ initialValue: feature.initialValue
195
+ });
196
+ continue;
197
+ }
198
+ if (sourceShape === "sync-function" || sourceShape === "derived" || sourceShape === "computed") {
199
+ sources.push({
200
+ kind: "derived",
201
+ sourceId,
202
+ dependencies
203
+ });
204
+ continue;
205
+ }
206
+ if (sourceShape === "async-function" || sourceShape === "async" || sourceShape === "promise-wrapper") {
207
+ if (dependencies.length === 0) {
208
+ addDiagnostic(diagnostics, "async-source-missing-tracked-dependencies", "Async signal sources need tracked dependencies.", {
209
+ pass: "signal-source-classification",
210
+ sourceId
211
+ });
212
+ }
213
+ sources.push({
214
+ kind: "async",
215
+ sourceId,
216
+ dependencies,
217
+ latest: feature.latest !== false,
218
+ pending: feature.pending !== false,
219
+ error: feature.error !== false,
220
+ versioned: true,
221
+ stream: feature.stream ?? "default"
222
+ });
223
+ continue;
224
+ }
225
+ if (sourceShape === "maybe-promise" || sourceShape === "maybe-promise-function") {
226
+ addDiagnostic(diagnostics, "maybe-promise-signal-source", "Maybe-promise signal sources must be made explicit before optimizer output is trusted.", {
227
+ pass: "signal-source-classification",
228
+ sourceId
229
+ });
230
+ continue;
231
+ }
232
+
233
+ addDiagnostic(diagnostics, "signal-source-unclassified", "signal(...) function source cannot be classified from static metadata.", {
234
+ pass: "signal-source-classification",
235
+ sourceId
236
+ });
237
+ }
238
+
239
+ return {
240
+ artifact: {
241
+ version: OPTIMIZER_ARTIFACT_VERSION,
242
+ sources
243
+ },
244
+ diagnostics
245
+ };
246
+ }
247
+
248
+ export function inferSignalOwnership(signalFeatures = [], options = {}) {
249
+ const diagnostics = getDiagnostics(options);
250
+ const ownership = [];
251
+
252
+ for (const feature of arrayOf(signalFeatures)) {
253
+ const sourceId = requireFeatureId(feature, "signal", diagnostics, "signal-ownership-lifetime");
254
+ if (!sourceId) {
255
+ continue;
256
+ }
257
+
258
+ if (feature.createdIn === "event-handler" && !feature.explicitLifetime) {
259
+ addDiagnostic(diagnostics, "invalid-handler-signal-lifetime", "Signals created inside event handlers need explicit lifetime before optimizer output.", {
260
+ pass: "signal-ownership-lifetime",
261
+ sourceId
262
+ });
263
+ continue;
264
+ }
265
+
266
+ const owner = normalizeOwner(feature.owner ?? feature.scope);
267
+ if (!owner) {
268
+ addDiagnostic(diagnostics, "ambiguous-signal-owner", "Signal ownership is ambiguous and must not be silently promoted to global.", {
269
+ pass: "signal-ownership-lifetime",
270
+ sourceId
271
+ });
272
+ continue;
273
+ }
274
+
275
+ ownership.push({
276
+ sourceId,
277
+ owner,
278
+ reason: feature.ownerReason ?? ownershipReason(owner)
279
+ });
280
+ }
281
+
282
+ return {
283
+ artifact: {
284
+ version: OPTIMIZER_ARTIFACT_VERSION,
285
+ ownership
286
+ },
287
+ diagnostics
288
+ };
289
+ }
290
+
291
+ export function extractEventSymbols(eventProps = [], options = {}) {
292
+ const diagnostics = getDiagnostics(options);
293
+ const events = [];
294
+ const handlers = [];
295
+
296
+ for (const [index, feature] of arrayOf(eventProps).entries()) {
297
+ const propName = feature.propName ?? feature.name;
298
+ const eventId = feature.eventId ?? `event:${index}`;
299
+ const handlerId = feature.handlerId ?? feature.handler ?? `${eventId}:handler`;
300
+
301
+ if (feature.syntax === "no-build" || String(propName).startsWith("on:")) {
302
+ addDiagnostic(diagnostics, "no-build-event-syntax-in-jsx", "JSX build profile uses onClick-style props unless compatibility mode is explicit.", {
303
+ pass: "event-symbol-extraction",
304
+ sourceId: eventId,
305
+ value: propName
306
+ });
307
+ continue;
308
+ }
309
+
310
+ const eventType = jsxEventType(propName);
311
+ if (!eventType) {
312
+ addDiagnostic(diagnostics, "event-command-unlowerable", "Event command cannot be statically lowered.", {
313
+ pass: "event-symbol-extraction",
314
+ sourceId: eventId,
315
+ value: propName
316
+ });
317
+ continue;
318
+ }
319
+
320
+ const commands = normalizeCommands(feature.commands, handlerId, feature.syncEventApis);
321
+ events.push({
322
+ eventId,
323
+ elementId: feature.elementId,
324
+ propName,
325
+ eventType,
326
+ commands,
327
+ handlerId
328
+ });
329
+ handlers.push({
330
+ symbolId: handlerId,
331
+ eventId,
332
+ propName,
333
+ eventType,
334
+ module: feature.module,
335
+ exportName: feature.exportName ?? handlerId,
336
+ mode: feature.emissionMode,
337
+ chunk: feature.chunk,
338
+ preload: Boolean(feature.preload),
339
+ syncEventApis: arrayOf(feature.syncEventApis),
340
+ preserveSyncEventApi: Boolean(feature.preserveSyncEventApi)
341
+ });
342
+ }
343
+
344
+ return {
345
+ artifact: {
346
+ version: OPTIMIZER_ARTIFACT_VERSION,
347
+ events,
348
+ handlers
349
+ },
350
+ diagnostics
351
+ };
352
+ }
353
+
354
+ export function lowerSuspenseReveal(semanticGraph = {}, options = {}) {
355
+ const diagnostics = getDiagnostics(options);
356
+ const suspenseBoundaries = [];
357
+ const suspenseById = new Map();
358
+
359
+ for (const [index, feature] of arrayOf(semanticGraph.suspense).entries()) {
360
+ const boundaryId = feature.boundaryId ?? feature.id;
361
+ if (!boundaryId) {
362
+ addDiagnostic(diagnostics, "unstable-suspense-boundary-id", "Suspense boundary cannot get a stable stream boundary id.", {
363
+ pass: "suspense-reveal-lowering",
364
+ sourceId: `suspense:${index}`
365
+ });
366
+ continue;
367
+ }
368
+ const boundary = {
369
+ boundaryId,
370
+ sourceOrder: Number(feature.sourceOrder ?? index),
371
+ fallbackId: feature.fallbackId,
372
+ finalId: feature.finalId,
373
+ asyncSourceIds: arrayOf(feature.asyncSourceIds)
374
+ };
375
+ suspenseBoundaries.push(boundary);
376
+ suspenseById.set(boundaryId, boundary);
377
+ }
378
+
379
+ const revealGroups = [];
380
+ for (const [index, policy] of arrayOf(semanticGraph.revealPolicies).entries()) {
381
+ const order = policy.order ?? "as-ready";
382
+ const tail = policy.tail ?? "visible";
383
+ const groupId = policy.groupId ?? `reveal:${index}`;
384
+ const boundaryIds = arrayOf(policy.boundaryIds);
385
+ if (!validRevealOrders.has(order) || !validRevealTails.has(tail)) {
386
+ addDiagnostic(diagnostics, "invalid-reveal-policy", "Invalid Reveal order or tail policy.", {
387
+ pass: "suspense-reveal-lowering",
388
+ sourceId: groupId,
389
+ value: `${order}/${tail}`
390
+ });
391
+ continue;
392
+ }
393
+
394
+ const boundaries = boundaryIds.map((boundaryId) => suspenseById.get(boundaryId));
395
+ if (boundaries.some((boundary) => !boundary)) {
396
+ addDiagnostic(diagnostics, "malformed-reveal-nesting", "Reveal policies must reference known direct Suspense boundaries.", {
397
+ pass: "suspense-reveal-lowering",
398
+ sourceId: groupId
399
+ });
400
+ continue;
401
+ }
402
+
403
+ const sourceOrder = boundaries
404
+ .toSorted((left, right) => left.sourceOrder - right.sourceOrder)
405
+ .map((boundary) => boundary.boundaryId);
406
+ const arrivalOrder = arrayOf(policy.arrivalOrder).filter((boundaryId) => boundaryIds.includes(boundaryId));
407
+ revealGroups.push({
408
+ groupId,
409
+ order,
410
+ tail,
411
+ boundaryIds,
412
+ arrivalOrder,
413
+ commitOrder: commitOrderForReveal(order, sourceOrder, arrivalOrder)
414
+ });
415
+ }
416
+
417
+ return {
418
+ artifact: {
419
+ version: OPTIMIZER_ARTIFACT_VERSION,
420
+ suspenseBoundaries,
421
+ revealGroups
422
+ },
423
+ diagnostics
424
+ };
425
+ }
426
+
427
+ export function selectRuntimeSlices(featureArtifacts = {}, options = {}) {
428
+ const signalSources = featureArtifacts.signalSources ?? { sources: [] };
429
+ const eventSymbols = featureArtifacts.eventSymbols ?? { events: [], handlers: [] };
430
+ const streamBoundaries = featureArtifacts.streamBoundaries ?? { suspenseBoundaries: [], revealGroups: [] };
431
+ const diagnostics = getDiagnostics(options);
432
+ const signalCounts = countBy(signalSources.sources, "kind");
433
+ const hasSignals = signalSources.sources.length > 0;
434
+ const hasAsyncSignals = signalCounts.async > 0;
435
+ const hasEvents = eventSymbols.events.length > 0;
436
+ const hasStream = streamBoundaries.suspenseBoundaries.length > 0 || streamBoundaries.revealGroups.length > 0;
437
+ const slices = [];
438
+
439
+ if (hasSignals) {
440
+ slices.push({ name: "signals", reason: "signal source artifact is non-empty" });
441
+ }
442
+ if (hasEvents) {
443
+ slices.push({ name: "events", reason: "event symbol artifact is non-empty" });
444
+ }
445
+ if (hasAsyncSignals) {
446
+ slices.push({ name: "async-signals", reason: "async signal source records require status/version state" });
447
+ }
448
+ if (hasStream) {
449
+ slices.push({ name: "stream", reason: "Suspense or Reveal artifacts require stream boundary coordination" });
450
+ }
451
+
452
+ const entrypoint = chooseRuntimeEntrypoint({ hasSignals, hasEvents, hasAsyncSignals, hasStream });
453
+ const selected = new Set(slices.map((slice) => slice.name));
454
+ const omitted = omittedRuntimeSystems.map((system) => ({
455
+ system,
456
+ reason: omittedSystemReason(system)
457
+ }));
458
+ if (!selected.has("async-signals")) {
459
+ omitted.push({ system: "async-signal-status", reason: "no async signal source records" });
460
+ }
461
+ if (!selected.has("stream")) {
462
+ omitted.push({ system: "stream-boundary-coordination", reason: "no Suspense, Reveal, boundary, or patch records" });
463
+ }
464
+
465
+ const fallbacks = [];
466
+ if (entrypoint === "no-build-loader") {
467
+ addDiagnostic(diagnostics, "runtime-slice-fallback", "Runtime selection must not silently fall back to the no-build loader.", {
468
+ pass: "runtime-slice-selection"
469
+ });
470
+ }
471
+
472
+ return {
473
+ artifact: {
474
+ version: OPTIMIZER_ARTIFACT_VERSION,
475
+ entrypoint,
476
+ slices,
477
+ omitted,
478
+ fallbacks
479
+ },
480
+ diagnostics
481
+ };
482
+ }
483
+
484
+ export function planHandlerEmission(handlers = [], options = {}) {
485
+ const diagnostics = getDiagnostics(options);
486
+ const decisions = [];
487
+
488
+ for (const handler of arrayOf(handlers)) {
489
+ const mode = handler.mode ?? (handler.module ? "direct-import" : "inline");
490
+ if (!validHandlerModes.has(mode)) {
491
+ addDiagnostic(diagnostics, "handler-emission-mode-unknown", "Handler emission mode is not supported.", {
492
+ pass: "handler-emission",
493
+ sourceId: handler.symbolId,
494
+ value: mode
495
+ });
496
+ continue;
497
+ }
498
+ if (mode === "lazy-chunk" && handler.syncEventApis.length > 0 && !handler.preserveSyncEventApi) {
499
+ addDiagnostic(diagnostics, "handler-emission-loses-event-semantics", "Lazy handler emission must preserve synchronous event API semantics.", {
500
+ pass: "handler-emission",
501
+ sourceId: handler.symbolId
502
+ });
503
+ continue;
504
+ }
505
+
506
+ const decision = { mode, symbolId: handler.symbolId };
507
+ if (mode === "direct-import") {
508
+ decision.module = handler.module;
509
+ }
510
+ if (mode === "eager-chunk" || mode === "lazy-chunk") {
511
+ decision.chunk = handler.chunk;
512
+ if (mode === "lazy-chunk" && handler.preload) {
513
+ decision.preload = true;
514
+ }
515
+ }
516
+ decisions.push(decision);
517
+ }
518
+
519
+ return {
520
+ artifact: {
521
+ version: OPTIMIZER_ARTIFACT_VERSION,
522
+ handlers: decisions
523
+ },
524
+ diagnostics
525
+ };
526
+ }
527
+
528
+ export function createOptimizerReport(artifacts) {
529
+ const signalSourceCounts = countBy(artifacts.signalSources.sources, "kind", ["writable", "derived", "async"]);
530
+ const signalOwnershipCounts = countBy(artifacts.signalOwnership.ownership, "owner", [
531
+ "app",
532
+ "shared-module",
533
+ "component",
534
+ "owner-relative"
535
+ ]);
536
+ const asyncStatusCounts = {
537
+ latest: countWhere(artifacts.signalSources.sources, (source) => source.kind === "async" && source.latest),
538
+ pending: countWhere(artifacts.signalSources.sources, (source) => source.kind === "async" && source.pending),
539
+ error: countWhere(artifacts.signalSources.sources, (source) => source.kind === "async" && source.error),
540
+ versioned: countWhere(artifacts.signalSources.sources, (source) => source.kind === "async" && source.versioned),
541
+ streamDefault: countWhere(artifacts.signalSources.sources, (source) => source.kind === "async" && source.stream === "default"),
542
+ streamDefer: countWhere(artifacts.signalSources.sources, (source) => source.kind === "async" && source.stream === "defer")
543
+ };
544
+ const handlerEmissionCounts = countBy(artifacts.handlerEmission.handlers, "mode", [
545
+ "inline",
546
+ "direct-import",
547
+ "eager-chunk",
548
+ "lazy-chunk"
549
+ ]);
550
+ const revealOrderCounts = countBy(artifacts.streamBoundaries.revealGroups, "order", [
551
+ "as-ready",
552
+ "forwards",
553
+ "backwards",
554
+ "together"
555
+ ]);
556
+ const revealTailCounts = countBy(artifacts.streamBoundaries.revealGroups, "tail", [
557
+ "visible",
558
+ "collapsed",
559
+ "hidden"
560
+ ]);
561
+
562
+ return {
563
+ version: OPTIMIZER_ARTIFACT_VERSION,
564
+ runtime: {
565
+ entrypoint: artifacts.runtimeSelection.entrypoint,
566
+ slices: artifacts.runtimeSelection.slices,
567
+ omitted: artifacts.runtimeSelection.omitted,
568
+ fallbacks: artifacts.runtimeSelection.fallbacks
569
+ },
570
+ signals: {
571
+ sources: signalSourceCounts,
572
+ ownership: signalOwnershipCounts,
573
+ asyncStatus: asyncStatusCounts
574
+ },
575
+ events: {
576
+ eventCount: artifacts.eventSymbols.events.length,
577
+ handlerCount: artifacts.eventSymbols.handlers.length
578
+ },
579
+ handlers: {
580
+ emission: handlerEmissionCounts
581
+ },
582
+ stream: {
583
+ suspenseBoundaryCount: artifacts.streamBoundaries.suspenseBoundaries.length,
584
+ reveal: {
585
+ byOrder: revealOrderCounts,
586
+ byTail: revealTailCounts
587
+ }
588
+ },
589
+ serverOnlyModuleExclusions: artifacts.sourceInventory.serverModules.length,
590
+ generatedLocatorCount: countGraphLocators(artifacts.jsxSemanticGraph)
591
+ };
592
+ }
593
+
594
+ export function hasOptimizerErrors(diagnostics = []) {
595
+ return arrayOf(diagnostics).some((diagnostic) => diagnostic.severity === "error");
596
+ }
597
+
598
+ function parseVersionMajor(version) {
599
+ const match = String(version ?? "").match(/^(\d+)/);
600
+ return match ? Number(match[1]) : 0;
601
+ }
602
+
603
+ function getDiagnostics(options) {
604
+ return Array.isArray(options.diagnostics) ? options.diagnostics : [];
605
+ }
606
+
607
+ function addDiagnostic(diagnostics, code, message, details = {}) {
608
+ diagnostics.push({
609
+ severity: "error",
610
+ code,
611
+ message,
612
+ ...details
613
+ });
614
+ }
615
+
616
+ function arrayOf(value) {
617
+ return Array.isArray(value) ? value : [];
618
+ }
619
+
620
+ function requireFeatureId(feature, label, diagnostics, pass) {
621
+ const id = feature.sourceId ?? feature.id;
622
+ if (id) {
623
+ return id;
624
+ }
625
+ addDiagnostic(diagnostics, `${label}-id-missing`, `${label} feature is missing a stable id.`, { pass });
626
+ return null;
627
+ }
628
+
629
+ function normalizeReads(reads) {
630
+ return arrayOf(reads).map((read) => {
631
+ if (typeof read === "string") {
632
+ return { sourceId: read };
633
+ }
634
+ return read;
635
+ });
636
+ }
637
+
638
+ function normalizeOwner(owner) {
639
+ if (owner === "global") {
640
+ return "app";
641
+ }
642
+ if (owner === "module") {
643
+ return "shared-module";
644
+ }
645
+ if (owner === "app" || owner === "shared-module" || owner === "component" || owner === "owner-relative") {
646
+ return owner;
647
+ }
648
+ return null;
649
+ }
650
+
651
+ function ownershipReason(owner) {
652
+ if (owner === "app") {
653
+ return "module-level signal is shared across the app graph";
654
+ }
655
+ if (owner === "shared-module") {
656
+ return "module-level signal is shared by imported modules";
657
+ }
658
+ if (owner === "component") {
659
+ return "signal is created inside component instance scope";
660
+ }
661
+ return "signal ownership is relative to the current owner";
662
+ }
663
+
664
+ function jsxEventType(propName) {
665
+ if (typeof propName !== "string" || !/^on[A-Z]/.test(propName)) {
666
+ return null;
667
+ }
668
+ return propName.slice(2).replace(/[A-Z]/g, (letter, index) => {
669
+ const lower = letter.toLowerCase();
670
+ return index === 0 ? lower : `-${lower}`;
671
+ });
672
+ }
673
+
674
+ function normalizeCommands(commands, handlerId, syncEventApis = []) {
675
+ const normalized = arrayOf(commands).map((command) => Array.isArray(command) ? command : [command]);
676
+ if (!normalized.some((command) => command[0] === "handler")) {
677
+ normalized.push(["handler", handlerId]);
678
+ }
679
+ for (const api of arrayOf(syncEventApis)) {
680
+ if (!normalized.some((command) => command[0] === api)) {
681
+ normalized.unshift([api]);
682
+ }
683
+ }
684
+ return normalized;
685
+ }
686
+
687
+ function commitOrderForReveal(order, sourceOrder, arrivalOrder) {
688
+ if (order === "as-ready") {
689
+ return arrivalOrder.length > 0 ? arrivalOrder : sourceOrder;
690
+ }
691
+ if (order === "backwards") {
692
+ return [...sourceOrder].reverse();
693
+ }
694
+ return sourceOrder;
695
+ }
696
+
697
+ function chooseRuntimeEntrypoint(features) {
698
+ if (features.hasSignals && !features.hasEvents && !features.hasAsyncSignals && !features.hasStream) {
699
+ return RUNTIME_ENTRYPOINTS.signals;
700
+ }
701
+ if (features.hasEvents && !features.hasSignals && !features.hasAsyncSignals && !features.hasStream) {
702
+ return RUNTIME_ENTRYPOINTS.events;
703
+ }
704
+ return RUNTIME_ENTRYPOINTS.runtime;
705
+ }
706
+
707
+ function omittedSystemReason(system) {
708
+ if (system === "no-build-loader") {
709
+ return "build profile emits an explicit runtime plan instead of scanning HTML through the no-build loader";
710
+ }
711
+ if (system === "server") {
712
+ return "server-only modules are excluded from browser output";
713
+ }
714
+ return "feature graph does not require this no-build system";
715
+ }
716
+
717
+ function countBy(items, key, knownKeys = []) {
718
+ const counts = Object.fromEntries(knownKeys.map((knownKey) => [knownKey, 0]));
719
+ for (const item of arrayOf(items)) {
720
+ const value = item[key];
721
+ counts[value] = (counts[value] ?? 0) + 1;
722
+ }
723
+ return counts;
724
+ }
725
+
726
+ function countWhere(items, predicate) {
727
+ return arrayOf(items).filter(predicate).length;
728
+ }
729
+
730
+ function countGraphLocators(graph) {
731
+ let total = arrayOf(graph.locators).length;
732
+ for (const component of arrayOf(graph.components)) {
733
+ total += arrayOf(component.locators).length;
734
+ }
735
+ return total;
736
+ }
@@ -0,0 +1,146 @@
1
+ import {
2
+ createOptimizerArtifactSet,
3
+ hasOptimizerErrors
4
+ } from "./build-optimizer.js";
5
+
6
+ export const BUILD_PROFILE_VERSION = 1;
7
+ export const BUILD_PROFILE_NAME = "build-required";
8
+ export const VIRTUAL_PLAN_ID = "virtual:async-framework/generated-plan";
9
+ export const RESOLVED_VIRTUAL_PLAN_ID = "\0virtual:async-framework/generated-plan";
10
+
11
+ export function createBuildProfileReport(fixture = {}, options = {}) {
12
+ const artifactSet = createOptimizerArtifactSet(fixture);
13
+ const diagnostics = [...artifactSet.diagnostics];
14
+ const plan = createBuildProfilePlan(fixture, artifactSet);
15
+ const bootstrap = createBuildProfileBootstrap(artifactSet.report.runtime.entrypoint);
16
+ const report = {
17
+ version: BUILD_PROFILE_VERSION,
18
+ profile: BUILD_PROFILE_NAME,
19
+ mode: options.mode ?? fixture.mode ?? "development",
20
+ host: normalizeBuildHost(fixture.sourceInventory?.config?.host),
21
+ runtime: artifactSet.report.runtime,
22
+ signals: artifactSet.report.signals,
23
+ events: artifactSet.report.events,
24
+ handlers: artifactSet.report.handlers,
25
+ stream: artifactSet.report.stream,
26
+ serverOnlyModuleExclusions: artifactSet.report.serverOnlyModuleExclusions,
27
+ generatedLocatorCount: artifactSet.report.generatedLocatorCount,
28
+ virtualModules: [VIRTUAL_PLAN_ID],
29
+ diagnostics
30
+ };
31
+
32
+ return {
33
+ version: BUILD_PROFILE_VERSION,
34
+ profile: BUILD_PROFILE_NAME,
35
+ artifacts: artifactSet.artifacts,
36
+ diagnostics,
37
+ failed: hasOptimizerErrors(diagnostics),
38
+ plan,
39
+ report,
40
+ bootstrap
41
+ };
42
+ }
43
+
44
+ export function createBuildProfilePlan(fixture = {}, artifactSet = createOptimizerArtifactSet(fixture)) {
45
+ if (fixture.runtimePlan) {
46
+ return normalizeRuntimePlan(fixture.runtimePlan);
47
+ }
48
+
49
+ const locators = artifactSet.artifacts.jsxSemanticGraph.locators;
50
+ const signalSources = artifactSet.artifacts.signalSources.sources;
51
+ const eventSymbols = artifactSet.artifacts.eventSymbols.events;
52
+ const elementIndex = new Map(locators.map((locator, index) => [locator, index]));
53
+ const plan = {
54
+ version: 1,
55
+ elements: locators,
56
+ signals: undefined,
57
+ events: undefined
58
+ };
59
+
60
+ const values = signalSources
61
+ .filter((source) => source.kind === "writable")
62
+ .map((source) => [source.sourceId, source.initialValue]);
63
+ if (values.length > 0) {
64
+ plan.signals = {
65
+ version: 1,
66
+ values,
67
+ bindings: fixture.signalBindings ?? []
68
+ };
69
+ }
70
+
71
+ if (eventSymbols.length > 0) {
72
+ plan.events = {
73
+ version: 1,
74
+ events: eventSymbols.map((event) => [
75
+ elementIndex.get(event.elementId) ?? 0,
76
+ event.eventType,
77
+ event.commands
78
+ ]),
79
+ handlers: fixture.handlers ?? {}
80
+ };
81
+ }
82
+
83
+ return withoutUndefined(plan);
84
+ }
85
+
86
+ export function createBuildProfileBootstrap(entrypoint) {
87
+ return {
88
+ entrypoint,
89
+ virtualPlan: VIRTUAL_PLAN_ID,
90
+ usesAsyncGlobal: false,
91
+ importsRootAsync: false
92
+ };
93
+ }
94
+
95
+ export function emitGeneratedPlanModule(profile) {
96
+ return [
97
+ "// Generated by @async/framework/vite. Do not edit.",
98
+ `export const plan = ${stableJson(profile.plan)};`,
99
+ `export const report = ${stableJson(profile.report)};`,
100
+ "export default plan;",
101
+ ""
102
+ ].join("\n");
103
+ }
104
+
105
+ export function emitBootstrapModule(profile) {
106
+ return [
107
+ "// Generated by @async/framework/vite. Do not edit.",
108
+ `import { start } from ${JSON.stringify(profile.bootstrap.entrypoint)};`,
109
+ `import { plan } from ${JSON.stringify(VIRTUAL_PLAN_ID)};`,
110
+ "",
111
+ "export { plan };",
112
+ "export function startAsyncFramework(root = document, options = {}) {",
113
+ " return start(root, plan, options);",
114
+ "}",
115
+ ""
116
+ ].join("\n");
117
+ }
118
+
119
+ function normalizeRuntimePlan(plan) {
120
+ if (!plan || typeof plan !== "object") {
121
+ throw new TypeError("Build profile runtime plan must be an object.");
122
+ }
123
+ if (plan.version !== 1) {
124
+ throw new Error(`Unsupported build profile runtime plan version: ${String(plan.version)}.`);
125
+ }
126
+ return {
127
+ version: 1,
128
+ ...plan
129
+ };
130
+ }
131
+
132
+ function normalizeBuildHost(host = {}) {
133
+ return {
134
+ name: host.name ?? "vite",
135
+ version: host.version ?? String(host.major ?? host.versionMajor ?? "8"),
136
+ engine: host.engine ?? "rolldown"
137
+ };
138
+ }
139
+
140
+ function withoutUndefined(value) {
141
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
142
+ }
143
+
144
+ function stableJson(value) {
145
+ return `${JSON.stringify(value, null, 2)}\n`;
146
+ }
package/jsx.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ // Generated by scripts/build-framework-bundle.js. Do not edit by hand.
2
+ // Type declarations for @async/framework/jsx.
3
+
4
+ export declare const ASYNC_JSX_SIGNAL: unique symbol;
5
+ export declare const ASYNC_JSX_COMPONENT: unique symbol;
6
+ export declare const ASYNC_JSX_SUSPENSE: unique symbol;
7
+ export declare const ASYNC_JSX_REVEAL: unique symbol;
8
+ export type JsxSignal<T = unknown> = { readonly kind: "async-jsx-signal"; readonly type: typeof ASYNC_JSX_SIGNAL; readonly source: T; readonly options: Record<string, unknown> };
9
+ export type JsxComponent<TProps extends Record<string, unknown> = Record<string, unknown>> = { readonly kind: "async-jsx-component"; readonly type: typeof ASYNC_JSX_COMPONENT; readonly render: (props: TProps) => unknown; readonly options: Record<string, unknown> };
10
+ export type JsxBoundary = { readonly kind: "async-jsx-suspense" | "async-jsx-reveal"; readonly type: typeof ASYNC_JSX_SUSPENSE | typeof ASYNC_JSX_REVEAL; readonly props: Record<string, unknown> };
11
+ export declare function signal<T = unknown>(source: T, options?: Record<string, unknown>): JsxSignal<T>;
12
+ export declare function component<TProps extends Record<string, unknown> = Record<string, unknown>>(render: (props: TProps) => unknown, options?: Record<string, unknown>): JsxComponent<TProps>;
13
+ export declare function Suspense(props?: Record<string, unknown>): JsxBoundary;
14
+ export declare function Reveal(props?: Record<string, unknown>): JsxBoundary;
package/jsx.js ADDED
@@ -0,0 +1,48 @@
1
+ export const ASYNC_JSX_SIGNAL = Symbol.for("async.framework.jsx.signal");
2
+ export const ASYNC_JSX_COMPONENT = Symbol.for("async.framework.jsx.component");
3
+ export const ASYNC_JSX_SUSPENSE = Symbol.for("async.framework.jsx.suspense");
4
+ export const ASYNC_JSX_REVEAL = Symbol.for("async.framework.jsx.reveal");
5
+
6
+ export function signal(source, options = {}) {
7
+ return Object.freeze({
8
+ kind: "async-jsx-signal",
9
+ type: ASYNC_JSX_SIGNAL,
10
+ source,
11
+ options: freezeOptions(options)
12
+ });
13
+ }
14
+
15
+ export function component(render, options = {}) {
16
+ if (typeof render !== "function") {
17
+ throw new TypeError("component(...) expects a render function.");
18
+ }
19
+ return Object.freeze({
20
+ kind: "async-jsx-component",
21
+ type: ASYNC_JSX_COMPONENT,
22
+ render,
23
+ options: freezeOptions(options)
24
+ });
25
+ }
26
+
27
+ export function Suspense(props = {}) {
28
+ return Object.freeze({
29
+ kind: "async-jsx-suspense",
30
+ type: ASYNC_JSX_SUSPENSE,
31
+ props: freezeOptions(props)
32
+ });
33
+ }
34
+
35
+ export function Reveal(props = {}) {
36
+ return Object.freeze({
37
+ kind: "async-jsx-reveal",
38
+ type: ASYNC_JSX_REVEAL,
39
+ props: freezeOptions(props)
40
+ });
41
+ }
42
+
43
+ function freezeOptions(options) {
44
+ if (!options || typeof options !== "object") {
45
+ return {};
46
+ }
47
+ return Object.freeze({ ...options });
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@async/framework",
3
- "version": "0.11.17",
3
+ "version": "0.11.19",
4
4
  "description": "No-build Loader app runtime with browser and server entrypoints, signals, command events, route partials, cache split, SSR activation, and streaming boundaries.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -60,6 +60,16 @@
60
60
  "import": "./server.js",
61
61
  "default": "./server.js"
62
62
  },
63
+ "./jsx": {
64
+ "types": "./jsx.d.ts",
65
+ "import": "./jsx.js",
66
+ "default": "./jsx.js"
67
+ },
68
+ "./vite": {
69
+ "types": "./vite.d.ts",
70
+ "import": "./vite.js",
71
+ "default": "./vite.js"
72
+ },
63
73
  "./runtime": {
64
74
  "types": "./runtime.d.ts",
65
75
  "import": "./runtime.js",
@@ -91,9 +101,15 @@
91
101
  "framework.ts",
92
102
  "package.json",
93
103
  "server.js",
104
+ "jsx.d.ts",
105
+ "vite.d.ts",
94
106
  "runtime.d.ts",
95
107
  "runtime/signals.d.ts",
96
108
  "runtime/events.d.ts",
109
+ "jsx.js",
110
+ "vite.js",
111
+ "build-profile.js",
112
+ "build-optimizer.js",
97
113
  "runtime.js",
98
114
  "runtime/signals.js",
99
115
  "runtime/events.js",
package/vite.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ // Generated by scripts/build-framework-bundle.js. Do not edit by hand.
2
+ // Type declarations for @async/framework/vite.
3
+
4
+ export type ViteRolldownHost = { readonly name?: string; readonly version?: string | number; readonly viteVersion?: string | number; readonly versionMajor?: string | number; readonly engine?: string; readonly builder?: string | { readonly name?: string }; readonly rolldown?: boolean };
5
+ export type AsyncFrameworkPluginOptions = { readonly fixture?: Record<string, unknown>; readonly mode?: "development" | "production" | string; readonly host?: ViteRolldownHost };
6
+ export type AsyncFrameworkVitePlugin = {
7
+ readonly name: "async-framework";
8
+ readonly enforce: "pre";
9
+ readonly asyncFramework: { readonly profile: Record<string, unknown>; readonly report: Record<string, unknown> };
10
+ configResolved(config: ViteRolldownHost): void;
11
+ resolveId(id: string): string | null;
12
+ load(id: string): string | null;
13
+ transform(code: string, id: string): { code: string; map: null } | null;
14
+ getAsyncFrameworkReport(): Record<string, unknown>;
15
+ };
16
+ export declare function asyncFramework(options?: AsyncFrameworkPluginOptions): AsyncFrameworkVitePlugin;
17
+ export declare function validateViteRolldownHost(host?: ViteRolldownHost): Required<Pick<ViteRolldownHost, "name" | "version" | "engine">>;
18
+ export declare function normalizeViteHost(host?: ViteRolldownHost): Required<Pick<ViteRolldownHost, "name" | "version" | "engine">>;
package/vite.js ADDED
@@ -0,0 +1,82 @@
1
+ import {
2
+ RESOLVED_VIRTUAL_PLAN_ID,
3
+ VIRTUAL_PLAN_ID,
4
+ createBuildProfileReport,
5
+ emitBootstrapModule,
6
+ emitGeneratedPlanModule
7
+ } from "./build-profile.js";
8
+
9
+ export function asyncFramework(options = {}) {
10
+ const host = normalizeViteHost(options.host);
11
+ validateViteRolldownHost(host);
12
+ const profile = createBuildProfileReport(options.fixture ?? {}, {
13
+ mode: options.mode ?? "development"
14
+ });
15
+
16
+ return {
17
+ name: "async-framework",
18
+ enforce: "pre",
19
+ asyncFramework: {
20
+ profile,
21
+ report: profile.report
22
+ },
23
+ configResolved(config) {
24
+ validateViteRolldownHost(normalizeViteHost(options.host ?? config));
25
+ },
26
+ resolveId(id) {
27
+ if (id === VIRTUAL_PLAN_ID) {
28
+ return RESOLVED_VIRTUAL_PLAN_ID;
29
+ }
30
+ return null;
31
+ },
32
+ load(id) {
33
+ if (id === RESOLVED_VIRTUAL_PLAN_ID) {
34
+ return emitGeneratedPlanModule(profile);
35
+ }
36
+ return null;
37
+ },
38
+ transform(code, id) {
39
+ if (!isJsxModule(id) || !usesAsyncJsx(code)) {
40
+ return null;
41
+ }
42
+ return {
43
+ code: emitBootstrapModule(profile),
44
+ map: null
45
+ };
46
+ },
47
+ getAsyncFrameworkReport() {
48
+ return profile.report;
49
+ }
50
+ };
51
+ }
52
+
53
+ export function validateViteRolldownHost(host = {}) {
54
+ const normalized = normalizeViteHost(host);
55
+ if (normalized.name !== "vite") {
56
+ throw new Error(`@async/framework/vite requires Vite 8+ with Rolldown; received ${normalized.name}.`);
57
+ }
58
+ if (Number.parseInt(normalized.version, 10) < 8) {
59
+ throw new Error(`@async/framework/vite requires Vite 8+; received ${normalized.version}.`);
60
+ }
61
+ if (normalized.engine !== "rolldown") {
62
+ throw new Error(`@async/framework/vite requires the Rolldown engine; received ${normalized.engine}.`);
63
+ }
64
+ return normalized;
65
+ }
66
+
67
+ export function normalizeViteHost(host = {}) {
68
+ const builder = host.builder && typeof host.builder === "object" ? host.builder.name : host.builder;
69
+ return {
70
+ name: host.name ?? "vite",
71
+ version: String(host.version ?? host.viteVersion ?? host.versionMajor ?? "8.0.0"),
72
+ engine: host.engine ?? builder ?? (host.rolldown === false ? "rollup" : "rolldown")
73
+ };
74
+ }
75
+
76
+ function isJsxModule(id) {
77
+ return /\.(?:jsx|tsx)$/.test(String(id));
78
+ }
79
+
80
+ function usesAsyncJsx(source) {
81
+ return /@async\/framework\/jsx/.test(source);
82
+ }