@pikku/core 0.12.96 → 0.12.97

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.
@@ -1,3 +1,4 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec'
1
2
  import type { VariablesService } from './variables-service.js'
2
3
 
3
4
  export interface VariableStatus {
@@ -5,13 +6,35 @@ export interface VariableStatus {
5
6
  name: string
6
7
  displayName: string
7
8
  isConfigured: boolean
9
+ /** Whether the declaration answers for itself when the host sets nothing. */
10
+ hasDefault: boolean
8
11
  }
9
12
 
10
13
  export type VariableMeta = {
11
14
  name: string
12
15
  displayName: string
16
+ /**
17
+ * The shape the variable was declared with. It is the schema itself rather
18
+ * than a description of it, because a default is only knowable by running it:
19
+ * `undefined` goes in and, if the declaration carries one, the default comes
20
+ * back out.
21
+ *
22
+ * A thunk is accepted, and is what code generation emits. The generated file
23
+ * and the file declaring the schema import each other, so reading the schema
24
+ * while the modules are still initializing throws — deferring the read until
25
+ * a variable is actually asked for is what keeps the cycle harmless.
26
+ */
27
+ schema?: StandardSchemaV1 | (() => StandardSchemaV1)
13
28
  }
14
29
 
30
+ const isPromise = (value: unknown): value is Promise<unknown> =>
31
+ typeof (value as Promise<unknown> | undefined)?.then === 'function'
32
+
33
+ /**
34
+ * A declared default is the answer to a variable nobody set, so it is resolved
35
+ * here rather than in `VariablesService`: the store knows what a host has put
36
+ * in it, and only this layer knows what was declared.
37
+ */
15
38
  export class TypedVariablesService<
16
39
  TMap = Record<string, unknown>,
17
40
  > implements VariablesService {
@@ -25,13 +48,23 @@ export class TypedVariablesService<
25
48
  ): Promise<TMap[K] | undefined> | TMap[K] | undefined
26
49
  get<T = string>(name: string): Promise<T | undefined> | T | undefined
27
50
  get(name: string): Promise<unknown> | unknown {
28
- return this.variables.get(name)
51
+ const stored = this.variables.get(name)
52
+ if (isPromise(stored)) {
53
+ return stored.then((value) =>
54
+ value === undefined ? this.resolveDefault(name) : value
55
+ )
56
+ }
57
+ return stored === undefined ? this.resolveDefault(name) : stored
29
58
  }
30
59
 
31
60
  getVariables<T extends Record<string, unknown> = Record<string, unknown>>(
32
61
  names: (keyof T & string)[]
33
62
  ): Promise<Partial<T>> | Partial<T> {
34
- return this.variables.getVariables<T>(names)
63
+ const stored = this.variables.getVariables<T>(names)
64
+ if (isPromise(stored)) {
65
+ return stored.then((values) => this.withDefaults(names, values))
66
+ }
67
+ return this.withDefaults(names, stored)
35
68
  }
36
69
 
37
70
  getAll():
@@ -62,14 +95,72 @@ export class TypedVariablesService<
62
95
  name: meta.name,
63
96
  displayName: meta.displayName,
64
97
  isConfigured: all[variableId] !== undefined,
98
+ hasDefault: (await this.resolveDefault(variableId)) !== undefined,
65
99
  })
66
100
  }
67
101
 
68
102
  return results
69
103
  }
70
104
 
105
+ /**
106
+ * What a deployment still has to be told. A variable that defaults is not on
107
+ * this list — it has a value, just not one anybody has to supply.
108
+ */
71
109
  async getMissing(): Promise<VariableStatus[]> {
72
110
  const all = await this.getAllStatus()
73
- return all.filter((v) => !v.isConfigured)
111
+ return all.filter((v) => !v.isConfigured && !v.hasDefault)
112
+ }
113
+
114
+ /**
115
+ * The value the declaration answers with when the host set nothing, or
116
+ * `undefined` when it does not answer for itself.
117
+ */
118
+ private resolveDefault(name: string): Promise<unknown> | unknown {
119
+ const declared = this.variablesMeta[name]?.schema
120
+ if (!declared) {
121
+ return undefined
122
+ }
123
+ const schema = typeof declared === 'function' ? declared() : declared
124
+ const result = schema['~standard'].validate(undefined)
125
+ if (isPromise(result)) {
126
+ return result.then(unwrapDefault)
127
+ }
128
+ return unwrapDefault(result)
129
+ }
130
+
131
+ /**
132
+ * Kept synchronous when the defaults resolve synchronously, so a caller that
133
+ * did not await `getVariables` before does not have to start.
134
+ */
135
+ private withDefaults<T extends Record<string, unknown>>(
136
+ names: (keyof T & string)[],
137
+ values: Partial<T>
138
+ ): Promise<Partial<T>> | Partial<T> {
139
+ const out: Record<string, unknown> = { ...values }
140
+ const pending: Promise<void>[] = []
141
+ for (const name of names) {
142
+ if (out[name] !== undefined) continue
143
+ const fallback = this.resolveDefault(name)
144
+ if (isPromise(fallback)) {
145
+ pending.push(
146
+ fallback.then((value) => {
147
+ if (value !== undefined) out[name] = value
148
+ })
149
+ )
150
+ } else if (fallback !== undefined) {
151
+ out[name] = fallback
152
+ }
153
+ }
154
+ if (pending.length > 0) {
155
+ return Promise.all(pending).then(() => out as Partial<T>)
156
+ }
157
+ return out as Partial<T>
74
158
  }
75
159
  }
160
+
161
+ /**
162
+ * A schema with no default rejects `undefined`, which is not a failure here —
163
+ * it is the answer that there is nothing to fall back to.
164
+ */
165
+ const unwrapDefault = (result: StandardSchemaV1.Result<unknown>) =>
166
+ result.issues ? undefined : result.value