-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathcontract-emit.ts
More file actions
322 lines (295 loc) · 12.4 KB
/
Copy pathcontract-emit.ts
File metadata and controls
322 lines (295 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
import { mkdir } from 'node:fs/promises';
import { loadConfig } from '@prisma-next/config-loader';
import type { Contract } from '@prisma-next/contract/types';
import { emit, getEmittedArtifactPaths } from '@prisma-next/emitter';
import { createControlStack } from '@prisma-next/framework-components/control';
import { abortable } from '@prisma-next/utils/abortable';
import { blindCast } from '@prisma-next/utils/casts';
import { ifDefined } from '@prisma-next/utils/defined';
import type { JsonObject } from '@prisma-next/utils/json';
import { dirname, join } from 'pathe';
import { errorContractConfigMissing, errorRuntime } from '../../utils/cli-errors';
import { queueEmitByOutput } from '../../utils/emit-queue';
import { toExtensionInputs } from '../../utils/extension-pack-inputs';
import { assertFrameworkComponentsCompatible } from '../../utils/framework-components';
import { publishContractArtifactPair } from '../../utils/publish-contract-artifact-pair';
import { validateContractDeps } from '../../utils/validate-contract-deps';
import { enrichContract } from '../contract-enrichment';
import type {
ContractEmitOptions,
ContractEmitResult,
ControlActionName,
OnControlProgress,
} from '../types';
const EMIT_ACTION: ControlActionName = 'emit';
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function startSpan(onProgress: OnControlProgress | undefined, spanId: string, label: string): void {
onProgress?.({ action: EMIT_ACTION, kind: 'spanStart', spanId, label });
}
function endSpan(
onProgress: OnControlProgress | undefined,
spanId: string,
outcome: 'ok' | 'error',
): void {
onProgress?.({ action: EMIT_ACTION, kind: 'spanEnd', spanId, outcome });
}
function failedToResolveContractSource(why: string, fix: string, meta?: Record<string, unknown>) {
return errorRuntime('Failed to resolve contract source', {
why,
fix,
...ifDefined('meta', meta),
});
}
type ValidatedProviderResult =
| { readonly ok: true; readonly value: unknown }
| { readonly ok: false; readonly error: ReturnType<typeof errorRuntime> };
function diagnosticLocationSuffix(diagnostic: Record<string, unknown>): string {
const sourceId = typeof diagnostic['sourceId'] === 'string' ? diagnostic['sourceId'] : undefined;
const span = isRecord(diagnostic['span']) ? diagnostic['span'] : undefined;
const start = span && isRecord(span['start']) ? span['start'] : undefined;
const line = start && typeof start['line'] === 'number' ? start['line'] : undefined;
const column = start && typeof start['column'] === 'number' ? start['column'] : undefined;
if (sourceId && line !== undefined && column !== undefined) {
return ` (${sourceId}:${line}:${column})`;
}
if (sourceId) {
return ` (${sourceId})`;
}
return '';
}
function mapDiagnosticsToIssues(
diagnostics: readonly unknown[],
): ReadonlyArray<{ readonly kind: string; readonly message: string }> {
const issues: { readonly kind: string; readonly message: string }[] = [];
for (const raw of diagnostics) {
if (!isRecord(raw)) continue;
const code = typeof raw['code'] === 'string' ? raw['code'] : 'diagnostic';
const message = typeof raw['message'] === 'string' ? raw['message'] : '';
issues.push({ kind: code, message: `${message}${diagnosticLocationSuffix(raw)}` });
}
return issues;
}
function validateProviderResult(providerResult: unknown): ValidatedProviderResult {
if (!isRecord(providerResult) || typeof providerResult['ok'] !== 'boolean') {
return {
ok: false,
error: failedToResolveContractSource(
'Contract source provider returned malformed result shape.',
'Ensure contract.source.load resolves to ok(Contract) or notOk({ summary, diagnostics }).',
),
};
}
if (providerResult['ok']) {
if (!('value' in providerResult)) {
return {
ok: false,
error: failedToResolveContractSource(
'Contract source provider returned malformed success result: missing value.',
'Ensure contract.source.load success payload is ok(Contract).',
),
};
}
return { ok: true, value: providerResult['value'] };
}
const failure = providerResult['failure'];
if (
!isRecord(failure) ||
typeof failure['summary'] !== 'string' ||
!Array.isArray(failure['diagnostics'])
) {
return {
ok: false,
error: failedToResolveContractSource(
'Contract source provider returned malformed failure result: expected summary and diagnostics.',
'Ensure contract.source.load failure payload is notOk({ summary, diagnostics, meta? }).',
),
};
}
return {
ok: false,
error: failedToResolveContractSource(
String(failure['summary']),
'Fix contract source diagnostics and return ok(Contract).',
{
diagnostics: failure['diagnostics'],
issues: mapDiagnosticsToIssues(failure['diagnostics']),
...ifDefined('providerMeta', failure['meta']),
},
),
};
}
/**
* Canonical contract emit operation.
*
* This is the SINGLE publication path used by both the CLI command
* (`prisma-next contract emit`) and the Vite plugin
* (`@prisma-next/vite-plugin-contract-emit`). New callers must go through this
* function rather than re-implementing load → emit → publish.
*
* The whole flow (load config → resolve source → emit bytes → atomic publish)
* is serialized per output JSON path via `queueEmitByOutput`. Concurrent calls
* for the same output line up FIFO; the user-visible outcome is "last
* submission wins on disk" without any supersession bookkeeping. Within a
* single emit, `publishContractArtifactPair` stages temp files, renames
* `contract.d.ts` before `contract.json`, and attempts to restore the previous
* pair if either rename fails — so type-only consumers never observe a
* mismatched pair.
*
* @throws {CliStructuredError} on config/source/validation problems
* @throws {DOMException} `AbortError` if cancelled via `signal`
*/
export async function executeContractEmit(
options: ContractEmitOptions,
): Promise<ContractEmitResult> {
const { configPath, outputPath, signal = new AbortController().signal, onProgress } = options;
const unlessAborted = abortable(signal);
const config = await unlessAborted(loadConfig(configPath));
if (!config.contract) {
throw errorContractConfigMissing({
why: 'Config.contract is required for emit. Define it in your config: contract: { source: ..., output: ... }',
});
}
const contractConfig = config.contract;
const effectiveOutput =
outputPath !== undefined ? join(outputPath, 'contract.json') : contractConfig.output;
if (!effectiveOutput) {
throw errorContractConfigMissing({
why: 'Contract config must have output path. This should not happen if defineConfig() was used.',
});
}
if (typeof contractConfig.source?.load !== 'function') {
throw errorContractConfigMissing({
why: 'Contract config must include a valid source provider object',
});
}
let outputPaths: ReturnType<typeof getEmittedArtifactPaths>;
try {
outputPaths = getEmittedArtifactPaths(effectiveOutput);
} catch (error) {
throw errorContractConfigMissing({
why: error instanceof Error ? error.message : String(error),
});
}
const { jsonPath: outputJsonPath, dtsPath: outputDtsPath } = outputPaths;
return queueEmitByOutput(outputJsonPath, async () => {
const stack = createControlStack(config);
const extensionInputs = toExtensionInputs(
blindCast<
readonly unknown[],
'toExtensionInputs accepts readonly unknown[] per its documented structural cast boundary'
>(stack.extensionPacks),
);
const composedExtensionContracts = new Map<string, Contract>(
extensionInputs
.filter((p) => p.contractSpace !== undefined)
.map((p) => [
p.id,
blindCast<
Contract,
'contractSpace.contractJson is the typed contract for this extension space'
>(p.contractSpace!.contractJson),
]),
);
const sourceContext = {
composedExtensionPacks: stack.extensionPacks.map((p) => p.id),
composedExtensionContracts,
scalarTypeDescriptors: stack.scalarTypeDescriptors,
authoringContributions: stack.authoringContributions,
codecLookup: stack.codecLookup,
controlMutationDefaults: stack.controlMutationDefaults,
resolvedInputs: contractConfig.source.inputs ?? [],
capabilities: stack.capabilities,
};
startSpan(onProgress, 'resolveSource', 'Resolving contract source...');
let providerResult: Awaited<ReturnType<typeof contractConfig.source.load>>;
try {
providerResult = await unlessAborted(contractConfig.source.load(sourceContext));
} catch (error) {
endSpan(onProgress, 'resolveSource', 'error');
if (signal.aborted || (isRecord(error) && error['name'] === 'AbortError')) {
throw error;
}
throw failedToResolveContractSource(
error instanceof Error ? error.message : String(error),
'Ensure contract.source.load resolves to ok(Contract) or returns structured diagnostics.',
);
}
const validatedContract = validateProviderResult(providerResult);
if (!validatedContract.ok) {
endSpan(onProgress, 'resolveSource', 'error');
throw validatedContract.error;
}
endSpan(onProgress, 'resolveSource', 'ok');
startSpan(onProgress, 'emit', 'Emitting contract...');
let emitResult: Awaited<ReturnType<typeof emit>>;
try {
const familyInstance = config.family.create(stack);
const rawComponents = [config.target, config.adapter, ...(config.extensionPacks ?? [])];
const frameworkComponents = assertFrameworkComponentsCompatible(
config.family.familyId,
config.target.targetId,
rawComponents,
);
// Blind cast: `validateProviderResult` upstream has already
// pinned `validatedContract.value` to the provider's loose
// `Contract` envelope, but the local `Contract` type at this
// call site is the precise structural interface. The cast just
// defers the structural check by one statement so `enrichContract`
// can decorate first; the subsequent serialize→deserialize round-trip
// re-narrows the envelope into the precise type.
const enrichedIR = enrichContract(
validatedContract.value as unknown as Contract,
frameworkComponents,
);
const rawContractJson = config.target.contractSerializer.serializeContract(enrichedIR);
const deserializedContract = familyInstance.deserializeContract(rawContractJson);
// Each target's descriptor ships a `contractSerializer` SPI; the
// framework canonicalizer threads its `serializeContract` so the
// on-disk JSON envelope is constructed by target-owned code
// rather than by walking the in-memory contract with
// `Object.entries` (which would leak runtime-only class API
// fields into the persisted shape). The optional `shouldPreserveEmpty`
// and `sortStorage` hooks let the family contribute storage-specific
// canonicalization rules without the framework importing family code.
const { contractSerializer } = config.target;
const serializeContract = (c: Contract): JsonObject =>
contractSerializer.serializeContract(c);
emitResult = await unlessAborted(
emit(deserializedContract, stack, config.family.emission, {
outputJsonPath,
serializeContract,
...ifDefined('shouldPreserveEmpty', contractSerializer.shouldPreserveEmpty),
...ifDefined('sortStorage', contractSerializer.sortStorage),
}),
);
} catch (error) {
endSpan(onProgress, 'emit', 'error');
throw error;
}
endSpan(onProgress, 'emit', 'ok');
await unlessAborted(mkdir(dirname(outputJsonPath), { recursive: true }));
await publishContractArtifactPair({
outputJsonPath,
outputDtsPath,
contractJson: emitResult.contractJson,
contractDts: emitResult.contractDts,
publicationToken: String(process.hrtime.bigint()),
});
const validationWarning = validateContractDeps(
emitResult.contractDts,
dirname(outputDtsPath),
).warning;
return {
storageHash: emitResult.storageHash,
...ifDefined('executionHash', emitResult.executionHash),
profileHash: emitResult.profileHash,
files: {
json: outputJsonPath,
dts: outputDtsPath,
},
...ifDefined('validationWarning', validationWarning),
};
});
}