@highstate/contract 0.19.1 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/component.ts CHANGED
@@ -2,12 +2,27 @@
2
2
 
3
3
  import type { Simplify } from "type-fest"
4
4
  import type { Entity, implementedTypes } from "./entity"
5
- import type { OptionalEmptyRecords, OptionalUndefinedFields, PartialKeys } from "./utils"
5
+ import type { OptionalEmptyRecords, PartialKeys } from "./utils"
6
6
  import { isNonNullish, mapValues, pickBy, uniqueBy } from "remeda"
7
7
  import { z } from "zod"
8
- import { boundaryInput, boundaryInputs, registerInstance } from "./evaluation"
8
+ import { boundaryInput, registerInstance } from "./evaluation"
9
9
  import { camelCaseToHumanReadable } from "./i18n"
10
- import { type InstanceId, type InstanceInput, type InstanceInputGroup, inputKey } from "./instance"
10
+ import {
11
+ type EntityInput,
12
+ type InstanceId,
13
+ type InstanceInput,
14
+ inputKey,
15
+ type MultipleInput,
16
+ type RequiredInput,
17
+ type RequiredMultipleInput,
18
+ type RuntimeInput,
19
+ } from "./instance"
20
+ import {
21
+ createDeepOutputAccessor,
22
+ createInput,
23
+ createMultipleInputAccessor,
24
+ createNonProvidedInput,
25
+ } from "./instance-input"
11
26
  import {
12
27
  fieldNameSchema,
13
28
  genericNameSchema,
@@ -77,6 +92,13 @@ export const componentInputSchema = z.object({
77
92
  */
78
93
  type: versionedNameSchema,
79
94
 
95
+ /**
96
+ * The input name this output type is derived from.
97
+ *
98
+ * If set, the output uses the referenced input as its fallback type source.
99
+ */
100
+ fromInput: fieldNameSchema.optional(),
101
+
80
102
  /**
81
103
  * Whether the input is required.
82
104
  */
@@ -107,33 +129,75 @@ export type FullComponentInputOptions = {
107
129
 
108
130
  export type ComponentInputOptions = Entity | FullComponentInputOptions
109
131
 
132
+ export type FromInputComponentOutputOptions<TInputName extends string = string> = {
133
+ fromInput: TInputName
134
+ meta?: PartialKeys<ComponentInput["meta"], "title">
135
+ }
136
+
137
+ export type ComponentOutputOptions<
138
+ TInputs extends Record<string, ComponentInputOptions> = Record<string, ComponentInputOptions>,
139
+ > = ComponentInputOptions | FromInputComponentOutputOptions<Extract<keyof TInputs, string>>
140
+
110
141
  type ComponentInputOptionsToOutputRef<T extends ComponentInputOptions> = T extends Entity
111
- ? InstanceInput<T[typeof implementedTypes]>
142
+ ? EntityInput<T>
112
143
  : T extends FullComponentInputOptions
113
- ? T["required"] extends false
114
- ? T["multiple"] extends true
115
- ? InstanceInput<T["entity"][typeof implementedTypes]>[] | undefined
116
- : InstanceInput<T["entity"][typeof implementedTypes]> | undefined
117
- : T["multiple"] extends true
118
- ? InstanceInput<T["entity"][typeof implementedTypes]>[]
119
- : InstanceInput<T["entity"][typeof implementedTypes]>
144
+ ? T["multiple"] extends true
145
+ ? RequiredMultipleInput<EntityInput<T["entity"]>>
146
+ : EntityInput<T["entity"]>
120
147
  : never
121
148
 
149
+ type MultipleInputConstructorValue<TEntity extends Entity> =
150
+ | MultipleInput<EntityInput<TEntity>>
151
+ | EntityInput<TEntity>[]
152
+ | InstanceInput[]
153
+ | Array<MultipleInput<EntityInput<TEntity>> | EntityInput<TEntity>[] | InstanceInput[]>
154
+
122
155
  /**
123
156
  * The type-level specification of a component input hold by the component model.
124
157
  */
125
- export type ComponentInputSpec = [entity: Entity, required: boolean, multiple: boolean]
158
+ export type ComponentInputSpec<TEntity extends Entity = Entity> = [
159
+ entity: TEntity,
160
+ required: boolean,
161
+ multiple: boolean,
162
+ ]
126
163
 
127
164
  export type ComponentInputOptionsToSpec<T extends ComponentInputOptions> = T extends Entity
128
- ? [T, true, false] // [Entity, required, multiple]
165
+ ? [T, true, false]
129
166
  : T extends FullComponentInputOptions
130
167
  ? T["required"] extends false
131
168
  ? T["multiple"] extends true
132
- ? [T["entity"], false, true]
133
- : [T["entity"], false, false]
169
+ ? ComponentInputSpec<T["entity"]> extends infer Spec
170
+ ? Spec extends ComponentInputSpec
171
+ ? [Spec[0], false, true]
172
+ : never
173
+ : never
174
+ : ComponentInputSpec<T["entity"]> extends infer Spec
175
+ ? Spec extends ComponentInputSpec
176
+ ? [Spec[0], false, false]
177
+ : never
178
+ : never
134
179
  : T["multiple"] extends true
135
- ? [T["entity"], true, true]
136
- : [T["entity"], true, false]
180
+ ? ComponentInputSpec<T["entity"]> extends infer Spec
181
+ ? Spec extends ComponentInputSpec
182
+ ? [Spec[0], true, true]
183
+ : never
184
+ : never
185
+ : ComponentInputSpec<T["entity"]> extends infer Spec
186
+ ? Spec extends ComponentInputSpec
187
+ ? [Spec[0], true, false]
188
+ : never
189
+ : never
190
+ : never
191
+
192
+ export type ComponentOutputOptionsToSpec<
193
+ TOutput extends ComponentOutputOptions<TInputs>,
194
+ TInputs extends Record<string, ComponentInputOptions>,
195
+ > = TOutput extends FromInputComponentOutputOptions<infer TInputName>
196
+ ? TInputName extends keyof TInputs
197
+ ? ComponentInputOptionsToSpec<TInputs[TInputName]>
198
+ : never
199
+ : TOutput extends ComponentInputOptions
200
+ ? ComponentInputOptionsToSpec<TOutput>
137
201
  : never
138
202
 
139
203
  export type ComponentInputOptionsMapToSpecMap<T extends Record<string, ComponentInputOptions>> =
@@ -141,14 +205,23 @@ export type ComponentInputOptionsMapToSpecMap<T extends Record<string, Component
141
205
  ? Record<string, never>
142
206
  : { [K in keyof T]: ComponentInputOptionsToSpec<T[K]> }
143
207
 
144
- type ComponentInputMapToValue<T extends Record<string, ComponentInputOptions>> =
145
- OptionalUndefinedFields<{
146
- [K in keyof T]: ComponentInputOptionsToOutputRef<T[K]>
147
- }>
208
+ type ComponentInputMapToValue<T extends Record<string, ComponentInputOptions>> = {
209
+ [K in keyof T]: ComponentInputOptionsToOutputRef<T[K]>
210
+ }
148
211
 
149
- type ComponentInputMapToReturnType<T extends Record<string, ComponentInputOptions>> =
150
- // biome-ignore lint/suspicious/noConfusingVoidType: this is return type
151
- T extends Record<string, never> ? void : ComponentInputMapToValue<T>
212
+ type ComponentOutputMapToCreateOutputValue<
213
+ TOutputs extends Record<string, ComponentOutputOptions<TInputs>>,
214
+ TInputs extends Record<string, ComponentInputOptions>,
215
+ > = {
216
+ [K in keyof TOutputs]: InputSpecToInputRef<ComponentOutputOptionsToSpec<TOutputs[K], TInputs>>
217
+ }
218
+
219
+ type ComponentOutputMapToReturnType<
220
+ TOutputs extends Record<string, ComponentOutputOptions<TInputs>>,
221
+ TInputs extends Record<string, ComponentInputOptions>,
222
+ > = TOutputs extends Record<string, never> // biome-ignore lint/suspicious/noConfusingVoidType: this is return type
223
+ ? void
224
+ : Partial<ComponentOutputMapToCreateOutputValue<TOutputs, TInputs>>
152
225
 
153
226
  export type ComponentParams<
154
227
  TArgs extends Record<string, ComponentArgumentOptions>,
@@ -174,7 +247,7 @@ export type ComponentOptions<
174
247
  TType extends VersionedName,
175
248
  TArgs extends Record<string, ComponentArgumentOptions>,
176
249
  TInputs extends Record<string, ComponentInputOptions>,
177
- TOutputs extends Record<string, ComponentInputOptions>,
250
+ TOutputs extends Record<string, ComponentOutputOptions<TInputs>>,
178
251
  > = {
179
252
  /**
180
253
  * The type of the component.
@@ -215,7 +288,9 @@ export type ComponentOptions<
215
288
  * Their names are not automatically prefixed with the component name,
216
289
  * so you must prefix them manually if needed or guarantee uniqueness in another way.
217
290
  */
218
- create: (params: ComponentParams<TArgs, TInputs>) => ComponentInputMapToReturnType<TOutputs>
291
+ create: (
292
+ params: ComponentParams<TArgs, TInputs>,
293
+ ) => ComponentOutputMapToReturnType<TOutputs, TInputs>
219
294
 
220
295
  /**
221
296
  * For internal use only.
@@ -290,25 +365,117 @@ export type ComponentModel = z.infer<typeof componentModelSchema>
290
365
 
291
366
  type InputSpecToInputRef<T extends ComponentInputSpec> = T[1] extends true
292
367
  ? T[2] extends true
293
- ? InstanceInput<T[0][typeof implementedTypes]>[]
294
- : InstanceInput<T[0][typeof implementedTypes]>
368
+ ? MultipleInputConstructorValue<T[0]>
369
+ : EntityInput<T[0]> | InstanceInput
295
370
  : T[2] extends true
296
- ? InstanceInput<T[0][typeof implementedTypes]>[] | undefined
297
- : InstanceInput<T[0][typeof implementedTypes]> | undefined
371
+ ? MultipleInputConstructorValue<T[0]>
372
+ : EntityInput<T[0]> | InstanceInput
298
373
 
299
- type InputSpecToOutputRef<T extends ComponentInputSpec> = T[2] extends true
300
- ? InstanceInput<T[0][typeof implementedTypes]>[]
301
- : InstanceInput<T[0][typeof implementedTypes]>
374
+ type InputSpecToUnitOutputRef<T extends ComponentInputSpec> = T[2] extends true
375
+ ? RequiredMultipleInput<EntityInput<T[0]>>
376
+ : RequiredInput<EntityInput<T[0]>>
377
+
378
+ type InputSpecToCompositeOutputRef<T extends ComponentInputSpec> = T[2] extends true
379
+ ? RequiredMultipleInput<EntityInput<T[0]>>
380
+ : EntityInput<T[0]>
302
381
 
303
382
  export type InputSpecMapToInputRefMap<TInputs extends Record<string, ComponentInputSpec>> =
304
383
  TInputs extends Record<string, [string, never, never]>
305
384
  ? Record<string, never>
306
- : { [K in keyof TInputs]: InputSpecToInputRef<TInputs[K]> }
385
+ : {
386
+ [K in keyof TInputs as TInputs[K][1] extends true ? K : never]-?: InputSpecToInputRef<
387
+ TInputs[K]
388
+ >
389
+ } & {
390
+ [K in keyof TInputs as TInputs[K][1] extends true ? never : K]?: InputSpecToInputRef<
391
+ TInputs[K]
392
+ >
393
+ }
307
394
 
308
- export type OutputRefMap<TInputs extends Record<string, ComponentInputSpec>> =
309
- TInputs extends Record<string, [string, never, never]>
310
- ? Record<string, never>
311
- : { [K in keyof TInputs]: InputSpecToOutputRef<TInputs[K]> }
395
+ export type OutputRefMap<
396
+ TInputs extends Record<string, ComponentInputSpec>,
397
+ TKind extends ComponentKind = "composite",
398
+ > = TInputs extends Record<string, [string, never, never]>
399
+ ? Record<string, never>
400
+ : {
401
+ [K in keyof TInputs]: TKind extends "unit"
402
+ ? InputSpecToUnitOutputRef<TInputs[K]>
403
+ : InputSpecToCompositeOutputRef<TInputs[K]>
404
+ }
405
+
406
+ type StaticOutputRefByKind<
407
+ TOutputSpec extends ComponentInputSpec,
408
+ TKind extends ComponentKind,
409
+ > = TKind extends "unit"
410
+ ? InputSpecToUnitOutputRef<TOutputSpec>
411
+ : InputSpecToCompositeOutputRef<TOutputSpec>
412
+
413
+ type ResolveForwardedOutputRef<
414
+ TForwardedInputName extends string,
415
+ TCallInputs extends Record<string, unknown>,
416
+ TFallbackSpec extends ComponentInputSpec,
417
+ TKind extends ComponentKind,
418
+ > = TForwardedInputName extends keyof TCallInputs
419
+ ? NonNullable<TCallInputs[TForwardedInputName]>
420
+ : StaticOutputRefByKind<TFallbackSpec, TKind>
421
+
422
+ type ResolveOutputRefForCall<
423
+ TOutputOption,
424
+ TCallInputs extends Record<string, unknown>,
425
+ TFallbackSpec extends ComponentInputSpec,
426
+ TKind extends ComponentKind,
427
+ > = TOutputOption extends FromInputComponentOutputOptions<infer TInputName>
428
+ ? ResolveForwardedOutputRef<TInputName, TCallInputs, TFallbackSpec, TKind>
429
+ : StaticOutputRefByKind<TFallbackSpec, TKind>
430
+
431
+ type ResolveOutputRefMapForCall<
432
+ TOutputSpecs extends Record<string, ComponentInputSpec>,
433
+ TOutputOptions extends Record<string, ComponentOutputOptions<TInputOptions>>,
434
+ TInputOptions extends Record<string, ComponentInputOptions>,
435
+ TCallInputs extends Record<string, unknown>,
436
+ TKind extends ComponentKind,
437
+ > = {
438
+ [K in keyof TOutputSpecs]: K extends keyof TOutputOptions
439
+ ? [TOutputOptions[K]] extends [never]
440
+ ? StaticOutputRefByKind<TOutputSpecs[K], TKind>
441
+ : ResolveOutputRefForCall<TOutputOptions[K], TCallInputs, TOutputSpecs[K], TKind>
442
+ : StaticOutputRefByKind<TOutputSpecs[K], TKind>
443
+ }
444
+
445
+ type IncludesAllExpectedTypes<
446
+ TProvided extends Record<string, unknown>,
447
+ TExpected extends Record<string, unknown>,
448
+ > = Exclude<keyof TExpected, keyof TProvided> extends never ? true : false
449
+
450
+ type ExtractProvidedInputTypes<TValue> = TValue extends RuntimeInput<infer TTypes, any>
451
+ ? TTypes
452
+ : TValue extends Array<infer TItem>
453
+ ? ExtractProvidedInputTypes<TItem>
454
+ : never
455
+
456
+ type ValidateProvidedInputValue<TValue, TExpectedSpec extends ComponentInputSpec> = [
457
+ ExtractProvidedInputTypes<TValue>,
458
+ ] extends [never]
459
+ ? TValue
460
+ : ExtractProvidedInputTypes<TValue> extends infer TProvidedTypes
461
+ ? TProvidedTypes extends Record<string, unknown>
462
+ ? IncludesAllExpectedTypes<
463
+ TProvidedTypes,
464
+ TExpectedSpec[0][typeof implementedTypes]
465
+ > extends true
466
+ ? TValue
467
+ : never
468
+ : never
469
+ : never
470
+
471
+ type ValidateCallInputs<
472
+ TCallInputs extends Record<string, unknown>,
473
+ TInputSpecs extends Record<string, ComponentInputSpec>,
474
+ > = {
475
+ [K in keyof TCallInputs]: K extends keyof TInputSpecs
476
+ ? ValidateProvidedInputValue<TCallInputs[K], TInputSpecs[K]>
477
+ : TCallInputs[K]
478
+ }
312
479
 
313
480
  export const originalCreate = Symbol("originalCreate")
314
481
  export const kind = Symbol("kind")
@@ -318,6 +485,12 @@ export type Component<
318
485
  TArgs extends Record<string, z.ZodType> = Record<string, never>,
319
486
  TInputs extends Record<string, ComponentInputSpec> = Record<string, never>,
320
487
  TOutputs extends Record<string, ComponentInputSpec> = Record<string, never>,
488
+ TKind extends ComponentKind = "composite",
489
+ TInputOptions extends Record<string, ComponentInputOptions> = Record<string, never>,
490
+ TOutputOptions extends Record<string, ComponentOutputOptions<TInputOptions>> = Record<
491
+ string,
492
+ never
493
+ >,
321
494
  > = {
322
495
  /**
323
496
  * The type of the component.
@@ -334,12 +507,14 @@ export type Component<
334
507
  */
335
508
  entities: Map<string, Entity>
336
509
 
337
- (
510
+ <TCallInputs extends InputSpecMapToInputRefMap<TInputs> = InputSpecMapToInputRefMap<TInputs>>(
338
511
  context: InputComponentParams<
339
512
  { [K in keyof TArgs]: z.input<TArgs[K]> },
340
513
  InputSpecMapToInputRefMap<TInputs>
341
- >,
342
- ): OutputRefMap<TOutputs>
514
+ > & {
515
+ inputs?: ValidateCallInputs<TCallInputs, TInputs>
516
+ },
517
+ ): ResolveOutputRefMapForCall<TOutputs, TOutputOptions, TInputOptions, TCallInputs, TKind>
343
518
 
344
519
  /**
345
520
  * The original create function.
@@ -353,14 +528,18 @@ export function defineComponent<
353
528
  TType extends VersionedName = VersionedName,
354
529
  TArgs extends Record<string, ComponentArgumentOptions> = Record<string, never>,
355
530
  TInputs extends Record<string, ComponentInputOptions> = Record<string, never>,
356
- TOutputs extends Record<string, ComponentInputOptions> = Record<string, never>,
531
+ TOutputs extends Record<string, ComponentOutputOptions<TInputs>> = Record<string, never>,
532
+ TKind extends ComponentKind = "composite",
357
533
  >(
358
- options: ComponentOptions<TType, TArgs, TInputs, TOutputs>,
534
+ options: ComponentOptions<TType, TArgs, TInputs, TOutputs> & { [kind]?: TKind },
359
535
  ): Component<
360
536
  TType,
361
537
  { [K in keyof TArgs]: ComponentArgumentOptionsToSchema<TArgs[K]> },
362
538
  { [K in keyof TInputs]: ComponentInputOptionsToSpec<TInputs[K]> },
363
- { [K in keyof TOutputs]: ComponentInputOptionsToSpec<TOutputs[K]> }
539
+ { [K in keyof TOutputs]: ComponentOutputOptionsToSpec<TOutputs[K], TInputs> },
540
+ TKind,
541
+ TInputs,
542
+ TOutputs
364
543
  > {
365
544
  try {
366
545
  componentModelSchema.shape.type.parse(options.type)
@@ -374,13 +553,14 @@ export function defineComponent<
374
553
 
375
554
  const entities = new Map<string, Entity>()
376
555
  const mapInput = createInputMapper(entities)
556
+ const mapOutput = createOutputMapper(options.inputs, entities)
377
557
 
378
558
  const model: ComponentModel = {
379
559
  type: options.type,
380
560
  kind: options[kind] ?? "composite",
381
561
  args: mapValues(options.args ?? {}, mapArgument),
382
562
  inputs: mapValues(options.inputs ?? {}, mapInput),
383
- outputs: mapValues(options.outputs ?? {}, mapInput),
563
+ outputs: mapValues(options.outputs ?? {}, mapOutput),
384
564
  meta: {
385
565
  ...options.meta,
386
566
  title: options.meta?.title || camelCaseToHumanReadable(parseVersionedName(options.type)[0]),
@@ -392,43 +572,45 @@ export function defineComponent<
392
572
  definitionHash: null!,
393
573
  }
394
574
 
395
- function create(
396
- params: InputComponentParams<any, Record<string, InstanceInput | InstanceInputGroup>>,
397
- ): any {
575
+ type RuntimeCreateInputGroup =
576
+ | RuntimeInput
577
+ | InstanceInput
578
+ | (Array<RuntimeCreateInputGroup> & Partial<{ [boundaryInput]: InstanceInput }>)
579
+
580
+ function create(params: InputComponentParams<any, Record<string, RuntimeCreateInputGroup>>): any {
398
581
  const { name, args = {}, inputs } = params
399
582
  const instanceId = getInstanceId(options.type, name)
400
583
 
401
- const flatInputs: Record<string, InstanceInputGroup> = {}
584
+ const flatInputs: Record<string, RuntimeInput[]> = {}
402
585
  const tracedInputs: Record<string, InstanceInput[]> = {}
586
+ const runtimeInputKey = (input: RuntimeInput): string => {
587
+ if (input.provided) {
588
+ return inputKey(input)
589
+ }
590
+
591
+ return `missing:${input[boundaryInput].instanceId}:${input[boundaryInput].output}`
592
+ }
403
593
 
404
594
  for (const [key, inputGroup] of Object.entries(inputs ?? {})) {
595
+ if (!(key in model.inputs)) {
596
+ continue
597
+ }
598
+
405
599
  if (!inputGroup) {
406
600
  continue
407
601
  }
408
602
 
409
603
  if (!Array.isArray(inputGroup)) {
410
- if (inputGroup[boundaryInput]) {
411
- tracedInputs[key] = [inputGroup[boundaryInput]]
412
- }
413
-
414
- flatInputs[key] = [inputGroup]
604
+ const normalizedInput = normalizeIncomingInput(inputGroup, key)
605
+ tracedInputs[key] = [normalizedInput[boundaryInput]]
606
+ flatInputs[key] = [normalizedInput]
415
607
  continue
416
608
  }
417
609
 
418
- // merge "boundaryInputs" attached to array with "boundaryInput"s attached to individual items
419
- const group: InstanceInput[] = [...(inputGroup[boundaryInputs] ?? [])]
420
- const inputs: InstanceInput[] = []
421
-
422
- for (const item of inputGroup.flat(1)) {
423
- if (item[boundaryInput]) {
424
- group.push(item[boundaryInput])
425
- }
426
-
427
- inputs.push(item)
428
- }
610
+ const { group, inputs } = normalizeIncomingInputGroup(inputGroup, key)
429
611
 
430
612
  tracedInputs[key] = uniqueBy(group, inputKey)
431
- flatInputs[key] = uniqueBy(inputs, inputKey)
613
+ flatInputs[key] = uniqueBy(inputs, runtimeInputKey)
432
614
  }
433
615
 
434
616
  return registerInstance(
@@ -440,38 +622,43 @@ export function defineComponent<
440
622
  name,
441
623
  args,
442
624
  inputs: tracedInputs,
443
- resolvedInputs: mapValues(flatInputs, inputs =>
444
- inputs.filter(input => !("provided" in input && input.provided === false)),
445
- ),
625
+ resolvedInputs: mapValues(flatInputs, inputs => {
626
+ return inputs
627
+ .filter(input => input.provided)
628
+ .map(input => ({
629
+ instanceId: input.instanceId,
630
+ output: input.output,
631
+ ...(input.path ? { path: input.path } : {}),
632
+ }))
633
+ }),
446
634
  },
447
635
  () => {
448
636
  const markedInputs = mapValues(model.inputs, (componentInput, key) => {
449
637
  if (!componentInput.multiple) {
450
- // for single component use first available input and attach boundaryInput
451
- // technically, caller can pass multiple inputs or array of inputs ignoring the TypeScript restriction,
452
- // but we don't care about that
453
638
  const input = flatInputs[key]?.[0]
454
639
 
455
- if (input) {
456
- // return the input with boundaryInput attached
457
- // the input can also be "OptionalInstanceInput" with "provided: false", but this is still correct
458
- return { ...input, [boundaryInput]: { instanceId: instanceId, output: key } }
640
+ if (input?.provided) {
641
+ return createDeepOutputAccessor({
642
+ ...input,
643
+ [boundaryInput]: { instanceId: instanceId, output: key },
644
+ })
459
645
  }
460
646
 
461
- // create a new "not provided" input with boundaryInput attached
462
- return { provided: false, [boundaryInput]: { instanceId: instanceId, output: key } }
647
+ return createNonProvidedInput({ instanceId: instanceId, output: key })
463
648
  }
464
649
 
465
- // then handle array of inputs
466
- // caller can provide array of array which will be flattened with boundaryInputs preserved
467
- // regardless of the number of inputs, we always attach boundaryInputs to the whole array
468
- const inputs = flatInputs[key] ?? []
469
- inputs[boundaryInputs] = [{ instanceId, output: key }]
650
+ const inputs = (flatInputs[key] ?? []).filter(isProvidedRuntimeInput).map(input =>
651
+ createDeepOutputAccessor({
652
+ ...input,
653
+ [boundaryInput]: { instanceId: instanceId, output: key },
654
+ }),
655
+ )
470
656
 
471
- return inputs
657
+ const multipleBoundary = { instanceId, output: key }
658
+ return createMultipleInputAccessor(inputs, multipleBoundary)
472
659
  })
473
660
 
474
- const outputs: Record<string, InstanceInput[]> =
661
+ const outputs: Record<string, RuntimeInput | MultipleInput | null | undefined> =
475
662
  options.create({
476
663
  id: instanceId,
477
664
  name,
@@ -479,9 +666,74 @@ export function defineComponent<
479
666
  inputs: markedInputs as any,
480
667
  }) ?? {}
481
668
 
482
- return mapValues(pickBy(outputs, isNonNullish), outputs => [outputs].flat(2))
669
+ const normalizedOutputs = normalizeCreateOutputs(outputs, model, instanceId)
670
+
671
+ return withDeepOutputAccessors(normalizedOutputs, model, instanceId)
483
672
  },
484
673
  )
674
+
675
+ function normalizeIncomingInput(
676
+ input: RuntimeInput | InstanceInput,
677
+ inputName: string,
678
+ ): RuntimeInput {
679
+ if (isStableInstanceInput(input)) {
680
+ return createInput(input)
681
+ }
682
+
683
+ const runtimeInput = input as RuntimeInput
684
+
685
+ if (runtimeInput.provided) {
686
+ const inputBoundary = runtimeInput[boundaryInput] ?? {
687
+ instanceId: runtimeInput.instanceId,
688
+ output: runtimeInput.output,
689
+ ...(runtimeInput.path ? { path: runtimeInput.path } : {}),
690
+ }
691
+
692
+ return createInput(runtimeInput, { boundary: inputBoundary })
693
+ }
694
+
695
+ return createNonProvidedInput(
696
+ runtimeInput[boundaryInput] ?? { instanceId, output: inputName },
697
+ )
698
+ }
699
+
700
+ function normalizeIncomingInputGroup(
701
+ inputGroup: RuntimeCreateInputGroup,
702
+ inputName: string,
703
+ ): { group: InstanceInput[]; inputs: RuntimeInput[] } {
704
+ const group: InstanceInput[] = []
705
+ const inputs: RuntimeInput[] = []
706
+
707
+ const visit = (value: RuntimeCreateInputGroup): void => {
708
+ if (Array.isArray(value)) {
709
+ const arrayBoundary = (value as Partial<{ [boundaryInput]: InstanceInput }>)[
710
+ boundaryInput
711
+ ]
712
+
713
+ if (arrayBoundary) {
714
+ group.push(arrayBoundary)
715
+ }
716
+
717
+ for (const item of value) {
718
+ if (!item) {
719
+ continue
720
+ }
721
+
722
+ visit(item)
723
+ }
724
+
725
+ return
726
+ }
727
+
728
+ const normalizedInput = normalizeIncomingInput(value, inputName)
729
+ group.push(normalizedInput[boundaryInput])
730
+ inputs.push(normalizedInput)
731
+ }
732
+
733
+ visit(inputGroup)
734
+
735
+ return { group, inputs }
736
+ }
485
737
  }
486
738
 
487
739
  try {
@@ -526,6 +778,131 @@ function processArgs(
526
778
  return validatedArgs
527
779
  }
528
780
 
781
+ function withDeepOutputAccessors(
782
+ outputs: Record<string, RuntimeInput[]>,
783
+ model: ComponentModel,
784
+ instanceId: InstanceId,
785
+ ): Record<string, RuntimeInput | MultipleInput> {
786
+ return mapValues(outputs, (outputGroup, outputName) => {
787
+ const outputSpec = model.outputs[outputName]
788
+ if (!outputSpec) {
789
+ return outputGroup[0]!
790
+ }
791
+
792
+ const normalizedOutputs = outputGroup.map(output => createDeepOutputAccessor(output))
793
+
794
+ if (outputSpec.multiple) {
795
+ return createMultipleInputAccessor(normalizedOutputs, {
796
+ instanceId,
797
+ output: outputName,
798
+ })
799
+ }
800
+
801
+ return normalizedOutputs[0]!
802
+ })
803
+ }
804
+
805
+ function isProvidedRuntimeInput(input: RuntimeInput): input is RequiredInput {
806
+ return input.provided
807
+ }
808
+
809
+ function normalizeCreateOutputGroup(
810
+ outputGroup: RuntimeInput | InstanceInput | Array<RuntimeInput | InstanceInput>,
811
+ instanceId: InstanceId,
812
+ outputName: string,
813
+ ): RuntimeInput[] {
814
+ return [outputGroup]
815
+ .flat(2)
816
+ .filter(isNonNullish)
817
+ .map(output => {
818
+ if (isStableInstanceInput(output)) {
819
+ return createInput(output)
820
+ }
821
+
822
+ const runtimeOutput = output as RuntimeInput
823
+
824
+ if (runtimeOutput.provided) {
825
+ const stableBoundary = runtimeOutput[boundaryInput] ?? {
826
+ instanceId: runtimeOutput.instanceId,
827
+ output: runtimeOutput.output,
828
+ ...(runtimeOutput.path ? { path: runtimeOutput.path } : {}),
829
+ }
830
+
831
+ return createInput(runtimeOutput, { boundary: stableBoundary })
832
+ }
833
+
834
+ return createNonProvidedInput(
835
+ runtimeOutput[boundaryInput] ?? { instanceId, output: outputName },
836
+ )
837
+ })
838
+ }
839
+
840
+ function normalizeCreateOutputs(
841
+ outputs: Record<string, RuntimeInput | MultipleInput | null | undefined>,
842
+ model: ComponentModel,
843
+ instanceId: InstanceId,
844
+ ): Record<string, RuntimeInput[]> {
845
+ const normalizedOutputs: Record<string, RuntimeInput[]> = {}
846
+
847
+ for (const [outputName, outputSpec] of Object.entries(model.outputs)) {
848
+ const outputGroup = outputs[outputName]
849
+
850
+ if (!isNonNullish(outputGroup)) {
851
+ if (model.kind === "unit") {
852
+ throw new Error(`Unit output "${outputName}" in instance "${instanceId}" must be provided`)
853
+ }
854
+
855
+ normalizedOutputs[outputName] = outputSpec.multiple
856
+ ? []
857
+ : [createNonProvidedInput({ instanceId, output: outputName })]
858
+ continue
859
+ }
860
+
861
+ const normalizedGroup = normalizeCreateOutputGroup(outputGroup, instanceId, outputName)
862
+
863
+ if (outputSpec.multiple && normalizedGroup.some(output => !output.provided)) {
864
+ throw new Error(
865
+ `Multiple output "${outputName}" in instance "${instanceId}" cannot contain non-provided items`,
866
+ )
867
+ }
868
+
869
+ if (model.kind === "unit") {
870
+ if (normalizedGroup.length === 0 || normalizedGroup.some(output => !output.provided)) {
871
+ throw new Error(`Unit output "${outputName}" in instance "${instanceId}" must be provided`)
872
+ }
873
+ }
874
+
875
+ normalizedOutputs[outputName] =
876
+ outputSpec.multiple || normalizedGroup.length > 0
877
+ ? normalizedGroup
878
+ : [createNonProvidedInput({ instanceId, output: outputName })]
879
+ }
880
+
881
+ for (const [outputName, outputGroup] of Object.entries(pickBy(outputs, isNonNullish))) {
882
+ if (!(outputName in model.outputs) || outputName in normalizedOutputs) {
883
+ continue
884
+ }
885
+
886
+ normalizedOutputs[outputName] = normalizeCreateOutputGroup(outputGroup, instanceId, outputName)
887
+ }
888
+
889
+ return normalizedOutputs
890
+ }
891
+
892
+ function isStableInstanceInput(value: unknown): value is InstanceInput {
893
+ if (!value || typeof value !== "object") {
894
+ return false
895
+ }
896
+
897
+ if ("provided" in value) {
898
+ return false
899
+ }
900
+
901
+ const input = value as Partial<InstanceInput>
902
+
903
+ return typeof input.instanceId === "string" && typeof input.output === "string"
904
+ }
905
+
529
906
  /**
530
907
  * Checks if the value is a Highstate component.
531
908
  *
@@ -602,6 +979,44 @@ export function createInputMapper(entities: Map<string, Entity>) {
602
979
  }
603
980
  }
604
981
 
982
+ function isFromInputOutputOptions(
983
+ value: ComponentOutputOptions,
984
+ ): value is FromInputComponentOutputOptions {
985
+ return typeof value === "object" && value !== null && "fromInput" in value
986
+ }
987
+
988
+ function createOutputMapper(
989
+ inputs: Record<string, ComponentInputOptions> | undefined,
990
+ entities: Map<string, Entity>,
991
+ ) {
992
+ const mapInput = createInputMapper(entities)
993
+
994
+ return (value: ComponentOutputOptions, key: string): ComponentInput => {
995
+ if (!isFromInputOutputOptions(value)) {
996
+ return mapInput(value, key)
997
+ }
998
+
999
+ const sourceInput = inputs?.[value.fromInput]
1000
+ if (!sourceInput) {
1001
+ throw new Error(
1002
+ `Output "${key}" references missing input "${value.fromInput}" via fromInput.`,
1003
+ )
1004
+ }
1005
+
1006
+ const mappedInput = mapInput(sourceInput, value.fromInput)
1007
+
1008
+ return {
1009
+ ...mappedInput,
1010
+ fromInput: value.fromInput,
1011
+ meta: {
1012
+ ...mappedInput.meta,
1013
+ ...value.meta,
1014
+ title: value.meta?.title || camelCaseToHumanReadable(key),
1015
+ },
1016
+ }
1017
+ }
1018
+ }
1019
+
605
1020
  /**
606
1021
  * Formats the instance id from the instance type and instance name.
607
1022
  *
@@ -635,6 +1050,26 @@ function toFullComponentInputOptions<T extends Record<string, ComponentInputOpti
635
1050
  return mapValues(inputs, input => ("entity" in input ? input : { entity: input })) as any
636
1051
  }
637
1052
 
1053
+ type ToFullComponentOutputOptions<T extends Record<string, ComponentOutputOptions>> = Simplify<{
1054
+ [K in keyof T]: T[K] extends Entity ? { entity: T[K] } : T[K]
1055
+ }>
1056
+
1057
+ function toFullComponentOutputOptions<T extends Record<string, ComponentOutputOptions>>(
1058
+ outputs: T,
1059
+ ): ToFullComponentOutputOptions<T> {
1060
+ return mapValues(outputs, output => {
1061
+ if (typeof output !== "object" || output === null) {
1062
+ return { entity: output }
1063
+ }
1064
+
1065
+ if ("entity" in output || "fromInput" in output) {
1066
+ return output
1067
+ }
1068
+
1069
+ return { entity: output }
1070
+ }) as any
1071
+ }
1072
+
638
1073
  /**
639
1074
  * The helper marker for component arguments.
640
1075
  *
@@ -662,10 +1097,10 @@ export function $inputs<T extends Record<string, ComponentInputOptions>>(
662
1097
  *
663
1098
  * Helps validating outputs types and gives the compiler a hint for generating `meta` fields.
664
1099
  */
665
- export function $outputs<T extends Record<string, ComponentInputOptions>>(
1100
+ export function $outputs<T extends Record<string, ComponentOutputOptions>>(
666
1101
  outputs: T,
667
- ): ToFullComponentInputOptions<T> {
668
- return toFullComponentInputOptions(outputs)
1102
+ ): ToFullComponentOutputOptions<T> {
1103
+ return toFullComponentOutputOptions(outputs)
669
1104
  }
670
1105
 
671
1106
  /**