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