@prisma-next/cli 0.5.0-dev.10 → 0.5.0-dev.12
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/README.md +37 -3
- package/dist/cli.mjs +7 -14
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-faKQqcix.mjs → client-enZIahga.mjs} +5 -5
- package/dist/{client-faKQqcix.mjs.map → client-enZIahga.mjs.map} +1 -1
- package/dist/commands/contract-emit.d.mts.map +1 -1
- package/dist/commands/contract-emit.mjs +2 -7
- package/dist/commands/contract-infer.mjs +2 -8
- package/dist/commands/db-init.mjs +7 -8
- package/dist/commands/db-init.mjs.map +1 -1
- package/dist/commands/db-schema.mjs +5 -8
- package/dist/commands/db-schema.mjs.map +1 -1
- package/dist/commands/db-sign.mjs +6 -7
- package/dist/commands/db-sign.mjs.map +1 -1
- package/dist/commands/db-update.mjs +7 -8
- package/dist/commands/db-update.mjs.map +1 -1
- package/dist/commands/db-verify.mjs +8 -9
- package/dist/commands/db-verify.mjs.map +1 -1
- package/dist/commands/migration-apply.mjs +6 -7
- package/dist/commands/migration-apply.mjs.map +1 -1
- package/dist/commands/migration-new.d.mts.map +1 -1
- package/dist/commands/migration-new.mjs +7 -7
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.mjs +8 -8
- package/dist/commands/migration-plan.mjs.map +1 -1
- package/dist/commands/migration-ref.d.mts +1 -1
- package/dist/commands/migration-ref.mjs +3 -3
- package/dist/commands/migration-show.d.mts +1 -1
- package/dist/commands/migration-show.d.mts.map +1 -1
- package/dist/commands/migration-show.mjs +6 -6
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.mjs +2 -7
- package/dist/{config-loader-C25b63rJ.mjs → config-loader-ih8ViDb_.mjs} +2 -2
- package/dist/{config-loader-C25b63rJ.mjs.map → config-loader-ih8ViDb_.mjs.map} +1 -1
- package/dist/config-loader.mjs +1 -1
- package/dist/contract-emit-DWtGQYCD.mjs +150 -0
- package/dist/contract-emit-DWtGQYCD.mjs.map +1 -0
- package/dist/contract-emit-LjzCoicC.mjs +4 -0
- package/dist/contract-emit-RZBWzkop.mjs +329 -0
- package/dist/contract-emit-RZBWzkop.mjs.map +1 -0
- package/dist/{contract-enrichment-CAOELa-H.mjs → contract-enrichment-4Ptgw3Pe.mjs} +1 -1
- package/dist/{contract-enrichment-CAOELa-H.mjs.map → contract-enrichment-4Ptgw3Pe.mjs.map} +1 -1
- package/dist/{contract-infer-DnY9fUw0.mjs → contract-infer-BjzkcwQt.mjs} +5 -5
- package/dist/{contract-infer-DnY9fUw0.mjs.map → contract-infer-BjzkcwQt.mjs.map} +1 -1
- package/dist/exports/control-api.d.mts +41 -16
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +5 -7
- package/dist/exports/index.mjs +3 -8
- package/dist/exports/index.mjs.map +1 -1
- package/dist/exports/init-output.mjs +1 -1
- package/dist/{extract-operation-statements-DsFfxXVZ.mjs → extract-operation-statements-CU-Pp4-N.mjs} +2 -2
- package/dist/{extract-operation-statements-DsFfxXVZ.mjs.map → extract-operation-statements-CU-Pp4-N.mjs.map} +1 -1
- package/dist/{extract-sql-ddl-D9UbZDyz.mjs → extract-sql-ddl-Bm0Mm0IT.mjs} +1 -1
- package/dist/{extract-sql-ddl-D9UbZDyz.mjs.map → extract-sql-ddl-Bm0Mm0IT.mjs.map} +1 -1
- package/dist/{framework-components-C6el-5x_.mjs → framework-components-Bgcre3Z6.mjs} +1 -1
- package/dist/{framework-components-C6el-5x_.mjs.map → framework-components-Bgcre3Z6.mjs.map} +1 -1
- package/dist/{init-jf33mNQ6.mjs → init-BKgjxw6r.mjs} +5 -5
- package/dist/{init-jf33mNQ6.mjs.map → init-BKgjxw6r.mjs.map} +1 -1
- package/dist/{inspect-live-schema-CqoZhKC1.mjs → inspect-live-schema-QklSDLt_.mjs} +5 -5
- package/dist/{inspect-live-schema-CqoZhKC1.mjs.map → inspect-live-schema-QklSDLt_.mjs.map} +1 -1
- package/dist/migration-cli.mjs +2 -2
- package/dist/{migration-command-scaffold-B40VaF-m.mjs → migration-command-scaffold-BfloSWPZ.mjs} +6 -6
- package/dist/{migration-command-scaffold-B40VaF-m.mjs.map → migration-command-scaffold-BfloSWPZ.mjs.map} +1 -1
- package/dist/{migration-status-CDgFxhAo.mjs → migration-status-C5VYA5r9.mjs} +8 -8
- package/dist/migration-status-C5VYA5r9.mjs.map +1 -0
- package/dist/{migrations-CKRMAKka.mjs → migrations-CSaDHNpB.mjs} +2 -2
- package/dist/{migrations-CKRMAKka.mjs.map → migrations-CSaDHNpB.mjs.map} +1 -1
- package/dist/{output-BpcQrnnq.mjs → output-BiO7kt87.mjs} +1 -1
- package/dist/{output-BpcQrnnq.mjs.map → output-BiO7kt87.mjs.map} +1 -1
- package/dist/{progress-adapter-DvQWB1nK.mjs → progress-adapter-DgRGldpT.mjs} +1 -1
- package/dist/{progress-adapter-DvQWB1nK.mjs.map → progress-adapter-DgRGldpT.mjs.map} +1 -1
- package/dist/{result-handler-DcV0QoTr.mjs → result-handler-BmVh8AeV.mjs} +5 -5
- package/dist/{result-handler-DcV0QoTr.mjs.map → result-handler-BmVh8AeV.mjs.map} +1 -1
- package/dist/{terminal-ui-C3ZLwQxK.mjs → terminal-ui-u2YgKghu.mjs} +1 -1
- package/dist/{terminal-ui-C3ZLwQxK.mjs.map → terminal-ui-u2YgKghu.mjs.map} +1 -1
- package/dist/{verify-Bkycc-Tf.mjs → verify-BumcH6Ry.mjs} +2 -2
- package/dist/{verify-Bkycc-Tf.mjs.map → verify-BumcH6Ry.mjs.map} +1 -1
- package/package.json +15 -15
- package/src/commands/contract-emit.ts +67 -163
- package/src/commands/migration-apply.ts +1 -1
- package/src/commands/migration-new.ts +4 -1
- package/src/commands/migration-plan.ts +1 -1
- package/src/commands/migration-show.ts +4 -1
- package/src/commands/migration-status.ts +6 -6
- package/src/control-api/operations/contract-emit.ts +198 -115
- package/src/control-api/types.ts +22 -3
- package/src/exports/control-api.ts +2 -1
- package/src/utils/command-helpers.ts +2 -2
- package/src/utils/emit-queue.ts +26 -0
- package/src/utils/formatters/graph-migration-mapper.ts +1 -1
- package/src/utils/publish-contract-artifact-pair.ts +134 -0
- package/dist/contract-emit-B5wnhTuF.mjs +0 -195
- package/dist/contract-emit-B5wnhTuF.mjs.map +0 -1
- package/dist/contract-emit-B9wkchud.mjs +0 -6
- package/dist/contract-emit-PeB96eHy.mjs +0 -122
- package/dist/contract-emit-PeB96eHy.mjs.map +0 -1
- package/dist/migration-status-CDgFxhAo.mjs.map +0 -1
- package/dist/validate-contract-deps-B_Cs29TL.mjs +0 -37
- package/dist/validate-contract-deps-B_Cs29TL.mjs.map +0 -1
- /package/dist/{cli-errors-D2NPMaxW.d.mts → cli-errors-BJLUczXT.d.mts} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir
|
|
1
|
+
import { mkdir } from 'node:fs/promises';
|
|
2
2
|
import type { Contract } from '@prisma-next/contract/types';
|
|
3
3
|
import { emit, getEmittedArtifactPaths } from '@prisma-next/emitter';
|
|
4
4
|
import { createControlStack } from '@prisma-next/framework-components/control';
|
|
@@ -7,57 +7,156 @@ import { ifDefined } from '@prisma-next/utils/defined';
|
|
|
7
7
|
import { dirname } from 'pathe';
|
|
8
8
|
import { loadConfig } from '../../config-loader';
|
|
9
9
|
import { errorContractConfigMissing, errorRuntime } from '../../utils/cli-errors';
|
|
10
|
+
import { queueEmitByOutput } from '../../utils/emit-queue';
|
|
10
11
|
import { assertFrameworkComponentsCompatible } from '../../utils/framework-components';
|
|
12
|
+
import { publishContractArtifactPair } from '../../utils/publish-contract-artifact-pair';
|
|
13
|
+
import { validateContractDeps } from '../../utils/validate-contract-deps';
|
|
11
14
|
import { enrichContract } from '../contract-enrichment';
|
|
12
|
-
import type {
|
|
15
|
+
import type {
|
|
16
|
+
ContractEmitOptions,
|
|
17
|
+
ContractEmitResult,
|
|
18
|
+
ControlActionName,
|
|
19
|
+
OnControlProgress,
|
|
20
|
+
} from '../types';
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
readonly summary: string;
|
|
16
|
-
readonly diagnostics: readonly unknown[];
|
|
17
|
-
readonly meta?: unknown;
|
|
18
|
-
}
|
|
22
|
+
const EMIT_ACTION: ControlActionName = 'emit';
|
|
19
23
|
|
|
20
24
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
21
25
|
return typeof value === 'object' && value !== null;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
function
|
|
25
|
-
|
|
28
|
+
function startSpan(onProgress: OnControlProgress | undefined, spanId: string, label: string): void {
|
|
29
|
+
onProgress?.({ action: EMIT_ACTION, kind: 'spanStart', spanId, label });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function endSpan(
|
|
33
|
+
onProgress: OnControlProgress | undefined,
|
|
34
|
+
spanId: string,
|
|
35
|
+
outcome: 'ok' | 'error',
|
|
36
|
+
): void {
|
|
37
|
+
onProgress?.({ action: EMIT_ACTION, kind: 'spanEnd', spanId, outcome });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function failedToResolveContractSource(why: string, fix: string, meta?: Record<string, unknown>) {
|
|
41
|
+
return errorRuntime('Failed to resolve contract source', {
|
|
42
|
+
why,
|
|
43
|
+
fix,
|
|
44
|
+
...ifDefined('meta', meta),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type ValidatedProviderResult =
|
|
49
|
+
| { readonly ok: true; readonly value: unknown }
|
|
50
|
+
| { readonly ok: false; readonly error: ReturnType<typeof errorRuntime> };
|
|
51
|
+
|
|
52
|
+
function diagnosticLocationSuffix(diagnostic: Record<string, unknown>): string {
|
|
53
|
+
const sourceId = typeof diagnostic['sourceId'] === 'string' ? diagnostic['sourceId'] : undefined;
|
|
54
|
+
const span = isRecord(diagnostic['span']) ? diagnostic['span'] : undefined;
|
|
55
|
+
const start = span && isRecord(span['start']) ? span['start'] : undefined;
|
|
56
|
+
const line = start && typeof start['line'] === 'number' ? start['line'] : undefined;
|
|
57
|
+
const column = start && typeof start['column'] === 'number' ? start['column'] : undefined;
|
|
58
|
+
if (sourceId && line !== undefined && column !== undefined) {
|
|
59
|
+
return ` (${sourceId}:${line}:${column})`;
|
|
60
|
+
}
|
|
61
|
+
if (sourceId) {
|
|
62
|
+
return ` (${sourceId})`;
|
|
63
|
+
}
|
|
64
|
+
return '';
|
|
26
65
|
}
|
|
27
66
|
|
|
28
|
-
function
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
67
|
+
function mapDiagnosticsToIssues(
|
|
68
|
+
diagnostics: readonly unknown[],
|
|
69
|
+
): ReadonlyArray<{ readonly kind: string; readonly message: string }> {
|
|
70
|
+
const issues: { readonly kind: string; readonly message: string }[] = [];
|
|
71
|
+
for (const raw of diagnostics) {
|
|
72
|
+
if (!isRecord(raw)) continue;
|
|
73
|
+
const code = typeof raw['code'] === 'string' ? raw['code'] : 'diagnostic';
|
|
74
|
+
const message = typeof raw['message'] === 'string' ? raw['message'] : '';
|
|
75
|
+
issues.push({ kind: code, message: `${message}${diagnosticLocationSuffix(raw)}` });
|
|
76
|
+
}
|
|
77
|
+
return issues;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function validateProviderResult(providerResult: unknown): ValidatedProviderResult {
|
|
81
|
+
if (!isRecord(providerResult) || typeof providerResult['ok'] !== 'boolean') {
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
error: failedToResolveContractSource(
|
|
85
|
+
'Contract source provider returned malformed result shape.',
|
|
86
|
+
'Ensure contract.source.load resolves to ok(Contract) or notOk({ summary, diagnostics }).',
|
|
87
|
+
),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (providerResult['ok']) {
|
|
92
|
+
if (!('value' in providerResult)) {
|
|
93
|
+
return {
|
|
94
|
+
ok: false,
|
|
95
|
+
error: failedToResolveContractSource(
|
|
96
|
+
'Contract source provider returned malformed success result: missing value.',
|
|
97
|
+
'Ensure contract.source.load success payload is ok(Contract).',
|
|
98
|
+
),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return { ok: true, value: providerResult['value'] };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const failure = providerResult['failure'];
|
|
105
|
+
if (
|
|
106
|
+
!isRecord(failure) ||
|
|
107
|
+
typeof failure['summary'] !== 'string' ||
|
|
108
|
+
!Array.isArray(failure['diagnostics'])
|
|
109
|
+
) {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
error: failedToResolveContractSource(
|
|
113
|
+
'Contract source provider returned malformed failure result: expected summary and diagnostics.',
|
|
114
|
+
'Ensure contract.source.load failure payload is notOk({ summary, diagnostics, meta? }).',
|
|
115
|
+
),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
error: failedToResolveContractSource(
|
|
121
|
+
String(failure['summary']),
|
|
122
|
+
'Fix contract source diagnostics and return ok(Contract).',
|
|
123
|
+
{
|
|
124
|
+
diagnostics: failure['diagnostics'],
|
|
125
|
+
issues: mapDiagnosticsToIssues(failure['diagnostics']),
|
|
126
|
+
...ifDefined('providerMeta', failure['meta']),
|
|
127
|
+
},
|
|
128
|
+
),
|
|
129
|
+
};
|
|
32
130
|
}
|
|
33
131
|
|
|
34
132
|
/**
|
|
35
|
-
*
|
|
133
|
+
* Canonical contract emit operation.
|
|
36
134
|
*
|
|
37
|
-
* This is
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* 4. Emits contract artifacts (JSON and DTS)
|
|
42
|
-
* 5. Writes files to the paths specified in config
|
|
135
|
+
* This is the SINGLE publication path used by both the CLI command
|
|
136
|
+
* (`prisma-next contract emit`) and the Vite plugin
|
|
137
|
+
* (`@prisma-next/vite-plugin-contract-emit`). New callers must go through this
|
|
138
|
+
* function rather than re-implementing load → emit → publish.
|
|
43
139
|
*
|
|
44
|
-
*
|
|
140
|
+
* The whole flow (load config → resolve source → emit bytes → atomic publish)
|
|
141
|
+
* is serialized per output JSON path via `queueEmitByOutput`. Concurrent calls
|
|
142
|
+
* for the same output line up FIFO; the user-visible outcome is "last
|
|
143
|
+
* submission wins on disk" without any supersession bookkeeping. Within a
|
|
144
|
+
* single emit, `publishContractArtifactPair` stages temp files, renames
|
|
145
|
+
* `contract.d.ts` before `contract.json`, and attempts to restore the previous
|
|
146
|
+
* pair if either rename fails — so type-only consumers never observe a
|
|
147
|
+
* mismatched pair.
|
|
45
148
|
*
|
|
46
|
-
* @
|
|
47
|
-
* @
|
|
48
|
-
* @throws If config loading fails, contract is invalid, or file I/O fails
|
|
49
|
-
* @throws signal.reason if cancelled via AbortSignal (typically DOMException with name 'AbortError')
|
|
149
|
+
* @throws {CliStructuredError} on config/source/validation problems
|
|
150
|
+
* @throws {DOMException} `AbortError` if cancelled via `signal`
|
|
50
151
|
*/
|
|
51
152
|
export async function executeContractEmit(
|
|
52
153
|
options: ContractEmitOptions,
|
|
53
154
|
): Promise<ContractEmitResult> {
|
|
54
|
-
const { configPath, signal = new AbortController().signal } = options;
|
|
155
|
+
const { configPath, signal = new AbortController().signal, onProgress } = options;
|
|
55
156
|
const unlessAborted = abortable(signal);
|
|
56
157
|
|
|
57
|
-
// Load config using the existing config loader
|
|
58
158
|
const config = await unlessAborted(loadConfig(configPath));
|
|
59
159
|
|
|
60
|
-
// Validate contract config is present
|
|
61
160
|
if (!config.contract) {
|
|
62
161
|
throw errorContractConfigMissing({
|
|
63
162
|
why: 'Config.contract is required for emit. Define it in your config: contract: { source: ..., output: ... }',
|
|
@@ -66,14 +165,12 @@ export async function executeContractEmit(
|
|
|
66
165
|
|
|
67
166
|
const contractConfig = config.contract;
|
|
68
167
|
|
|
69
|
-
// Validate output path is present and ends with .json
|
|
70
168
|
if (!contractConfig.output) {
|
|
71
169
|
throw errorContractConfigMissing({
|
|
72
170
|
why: 'Contract config must have output path. This should not happen if defineConfig() was used.',
|
|
73
171
|
});
|
|
74
172
|
}
|
|
75
173
|
|
|
76
|
-
// Validate source exists and is callable
|
|
77
174
|
if (typeof contractConfig.source?.load !== 'function') {
|
|
78
175
|
throw errorContractConfigMissing({
|
|
79
176
|
why: 'Contract config must include a valid source provider object',
|
|
@@ -90,98 +187,84 @@ export async function executeContractEmit(
|
|
|
90
187
|
}
|
|
91
188
|
const { jsonPath: outputJsonPath, dtsPath: outputDtsPath } = outputPaths;
|
|
92
189
|
|
|
93
|
-
|
|
190
|
+
return queueEmitByOutput(outputJsonPath, async () => {
|
|
191
|
+
const stack = createControlStack(config);
|
|
94
192
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
193
|
+
const sourceContext = {
|
|
194
|
+
composedExtensionPacks: stack.extensionPacks.map((p) => p.id),
|
|
195
|
+
scalarTypeDescriptors: stack.scalarTypeDescriptors,
|
|
196
|
+
authoringContributions: stack.authoringContributions,
|
|
197
|
+
codecLookup: stack.codecLookup,
|
|
198
|
+
controlMutationDefaults: stack.controlMutationDefaults,
|
|
199
|
+
resolvedInputs: contractConfig.source.inputs ?? [],
|
|
200
|
+
};
|
|
103
201
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
202
|
+
startSpan(onProgress, 'resolveSource', 'Resolving contract source...');
|
|
203
|
+
let providerResult: Awaited<ReturnType<typeof contractConfig.source.load>>;
|
|
204
|
+
try {
|
|
205
|
+
providerResult = await unlessAborted(contractConfig.source.load(sourceContext));
|
|
206
|
+
} catch (error) {
|
|
207
|
+
endSpan(onProgress, 'resolveSource', 'error');
|
|
208
|
+
if (signal.aborted || (isRecord(error) && error['name'] === 'AbortError')) {
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
throw failedToResolveContractSource(
|
|
212
|
+
error instanceof Error ? error.message : String(error),
|
|
213
|
+
'Ensure contract.source.load resolves to ok(Contract) or returns structured diagnostics.',
|
|
214
|
+
);
|
|
110
215
|
}
|
|
111
|
-
throw errorRuntime('Failed to resolve contract source', {
|
|
112
|
-
why: error instanceof Error ? error.message : String(error),
|
|
113
|
-
fix: 'Ensure contract.source.load resolves to ok(Contract) or returns structured diagnostics.',
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
216
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (providerResult.ok && !('value' in providerResult)) {
|
|
125
|
-
throw errorRuntime('Failed to resolve contract source', {
|
|
126
|
-
why: 'Contract source provider returned malformed success result: missing value.',
|
|
127
|
-
fix: 'Ensure contract.source.load success payload is ok(Contract).',
|
|
128
|
-
});
|
|
129
|
-
}
|
|
217
|
+
const validatedContract = validateProviderResult(providerResult);
|
|
218
|
+
if (!validatedContract.ok) {
|
|
219
|
+
endSpan(onProgress, 'resolveSource', 'error');
|
|
220
|
+
throw validatedContract.error;
|
|
221
|
+
}
|
|
222
|
+
endSpan(onProgress, 'resolveSource', 'ok');
|
|
130
223
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
224
|
+
startSpan(onProgress, 'emit', 'Emitting contract...');
|
|
225
|
+
let emitResult: Awaited<ReturnType<typeof emit>>;
|
|
226
|
+
try {
|
|
227
|
+
const familyInstance = config.family.create(stack);
|
|
228
|
+
const rawComponents = [config.target, config.adapter, ...(config.extensionPacks ?? [])];
|
|
229
|
+
const frameworkComponents = assertFrameworkComponentsCompatible(
|
|
230
|
+
config.family.familyId,
|
|
231
|
+
config.target.targetId,
|
|
232
|
+
rawComponents,
|
|
233
|
+
);
|
|
234
|
+
const enrichedIR = enrichContract(validatedContract.value as Contract, frameworkComponents);
|
|
235
|
+
familyInstance.validateContract(enrichedIR);
|
|
236
|
+
emitResult = await unlessAborted(
|
|
237
|
+
emit(enrichedIR, stack, config.family.emission, { outputJsonPath }),
|
|
238
|
+
);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
endSpan(onProgress, 'emit', 'error');
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
endSpan(onProgress, 'emit', 'ok');
|
|
137
244
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
},
|
|
245
|
+
await unlessAborted(mkdir(dirname(outputJsonPath), { recursive: true }));
|
|
246
|
+
await publishContractArtifactPair({
|
|
247
|
+
outputJsonPath,
|
|
248
|
+
outputDtsPath,
|
|
249
|
+
contractJson: emitResult.contractJson,
|
|
250
|
+
contractDts: emitResult.contractDts,
|
|
251
|
+
publicationToken: String(process.hrtime.bigint()),
|
|
146
252
|
});
|
|
147
|
-
}
|
|
148
253
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
config.family.familyId,
|
|
154
|
-
config.target.targetId,
|
|
155
|
-
rawComponents,
|
|
156
|
-
);
|
|
157
|
-
const enrichedIR = enrichContract(providerResult.value as Contract, frameworkComponents);
|
|
158
|
-
|
|
159
|
-
familyInstance.validateContract(enrichedIR);
|
|
160
|
-
const emitResult = await unlessAborted(
|
|
161
|
-
emit(enrichedIR, stack, config.family.emission, {
|
|
162
|
-
outputJsonPath: outputJsonPath,
|
|
163
|
-
}),
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
// Create directory if needed and write files (both colocated)
|
|
167
|
-
await unlessAborted(mkdir(dirname(outputJsonPath), { recursive: true }));
|
|
168
|
-
await unlessAborted(writeFile(outputJsonPath, emitResult.contractJson, 'utf-8'));
|
|
169
|
-
await unlessAborted(writeFile(outputDtsPath, emitResult.contractDts, 'utf-8'));
|
|
170
|
-
|
|
171
|
-
// Validate that contract.d.ts type imports are resolvable
|
|
172
|
-
const { validateContractDeps } = await import('../../utils/validate-contract-deps');
|
|
173
|
-
const depsValidation = validateContractDeps(emitResult.contractDts, dirname(outputDtsPath));
|
|
174
|
-
if (depsValidation.warning) {
|
|
175
|
-
process.stderr.write(`\n⚠ ${depsValidation.warning}\n`);
|
|
176
|
-
}
|
|
254
|
+
const validationWarning = validateContractDeps(
|
|
255
|
+
emitResult.contractDts,
|
|
256
|
+
dirname(outputDtsPath),
|
|
257
|
+
).warning;
|
|
177
258
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
259
|
+
return {
|
|
260
|
+
storageHash: emitResult.storageHash,
|
|
261
|
+
...ifDefined('executionHash', emitResult.executionHash),
|
|
262
|
+
profileHash: emitResult.profileHash,
|
|
263
|
+
files: {
|
|
264
|
+
json: outputJsonPath,
|
|
265
|
+
dts: outputDtsPath,
|
|
266
|
+
},
|
|
267
|
+
...ifDefined('validationWarning', validationWarning),
|
|
268
|
+
};
|
|
269
|
+
});
|
|
187
270
|
}
|
package/src/control-api/types.ts
CHANGED
|
@@ -512,18 +512,31 @@ export type MigrationApplyResult = Result<MigrationApplySuccess, MigrationApplyF
|
|
|
512
512
|
|
|
513
513
|
/**
|
|
514
514
|
* Options for the standalone executeContractEmit function.
|
|
515
|
-
*
|
|
516
|
-
*
|
|
515
|
+
*
|
|
516
|
+
* `executeContractEmit` is the canonical publication path for both the
|
|
517
|
+
* `prisma-next contract emit` CLI command and the `@prisma-next/vite-plugin-contract-emit`
|
|
518
|
+
* Vite plugin. Do not duplicate the load → emit → publish dance elsewhere; if a
|
|
519
|
+
* caller needs additional behavior, extend this options shape and update the
|
|
520
|
+
* single implementation rather than building a parallel publication path.
|
|
521
|
+
*
|
|
522
|
+
* Concurrent calls for the same output JSON path are serialized per-output via
|
|
523
|
+
* a FIFO queue; concurrent calls for distinct outputs run in parallel.
|
|
517
524
|
*/
|
|
518
525
|
export interface ContractEmitOptions {
|
|
519
526
|
/** Path to the prisma-next.config.ts file */
|
|
520
527
|
readonly configPath: string;
|
|
521
|
-
/** Optional AbortSignal for
|
|
528
|
+
/** Optional AbortSignal for cancelling the in-flight emit */
|
|
522
529
|
readonly signal?: AbortSignal;
|
|
530
|
+
/** Optional progress callback for observing source-resolution and emit spans */
|
|
531
|
+
readonly onProgress?: OnControlProgress;
|
|
523
532
|
}
|
|
524
533
|
|
|
525
534
|
/**
|
|
526
535
|
* Result from the standalone executeContractEmit function.
|
|
536
|
+
*
|
|
537
|
+
* Always describes the bytes that were just published to disk. Failures throw
|
|
538
|
+
* (config / source-resolution / emit / publish) — callers do not need to
|
|
539
|
+
* branch on a result discriminator.
|
|
527
540
|
*/
|
|
528
541
|
export interface ContractEmitResult {
|
|
529
542
|
/** Hash of the storage contract (schema-level) */
|
|
@@ -539,6 +552,12 @@ export interface ContractEmitResult {
|
|
|
539
552
|
/** Path to the emitted contract.d.ts file */
|
|
540
553
|
readonly dts: string;
|
|
541
554
|
};
|
|
555
|
+
/**
|
|
556
|
+
* Warning surfaced by `validateContractDeps` after a successful publication.
|
|
557
|
+
* Callers (CLI, Vite plugin) decide how to render this; the operation does
|
|
558
|
+
* not write to stderr itself. Undefined when no warning was raised.
|
|
559
|
+
*/
|
|
560
|
+
readonly validationWarning?: string;
|
|
542
561
|
}
|
|
543
562
|
|
|
544
563
|
// ============================================================================
|
|
@@ -23,7 +23,6 @@ export { enrichContract } from '../control-api/contract-enrichment';
|
|
|
23
23
|
|
|
24
24
|
// Standalone operations (for tooling that doesn't need full client)
|
|
25
25
|
export { executeContractEmit } from '../control-api/operations/contract-emit';
|
|
26
|
-
|
|
27
26
|
// CLI-specific types
|
|
28
27
|
export type {
|
|
29
28
|
ContractEmitOptions,
|
|
@@ -54,3 +53,5 @@ export type {
|
|
|
54
53
|
SignOptions,
|
|
55
54
|
VerifyOptions,
|
|
56
55
|
} from '../control-api/types';
|
|
56
|
+
// Lifecycle helpers for hosts that publish to many output paths
|
|
57
|
+
export { disposeEmitQueue } from '../utils/emit-queue';
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import type { ControlTargetDescriptor } from '@prisma-next/framework-components/control';
|
|
3
3
|
import { hasMigrations } from '@prisma-next/framework-components/control';
|
|
4
|
-
import type { PathDecision } from '@prisma-next/migration-tools/dag';
|
|
5
|
-
import { reconstructGraph } from '@prisma-next/migration-tools/dag';
|
|
6
4
|
import type { MigrationGraph } from '@prisma-next/migration-tools/graph';
|
|
7
5
|
import { readMigrationsDir } from '@prisma-next/migration-tools/io';
|
|
6
|
+
import type { PathDecision } from '@prisma-next/migration-tools/migration-graph';
|
|
7
|
+
import { reconstructGraph } from '@prisma-next/migration-tools/migration-graph';
|
|
8
8
|
import type { MigrationPackage } from '@prisma-next/migration-tools/package';
|
|
9
9
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
10
10
|
import type { Command } from 'commander';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-output FIFO queue for `executeContractEmit`.
|
|
3
|
+
*
|
|
4
|
+
* Ensures that at most one emit (load → resolve source → emit bytes → publish)
|
|
5
|
+
* runs per output JSON path at a time. Concurrent calls for the same path
|
|
6
|
+
* line up behind the in-flight one and run in submission order; the user-visible
|
|
7
|
+
* outcome is "last submission wins on disk" without any supersession bookkeeping.
|
|
8
|
+
*
|
|
9
|
+
* Long-lived hosts (Vite dev server, watch CLIs) must call `disposeEmitQueue`
|
|
10
|
+
* when they stop publishing to a path, otherwise the module-global `Map`
|
|
11
|
+
* accumulates one entry per unique output path for the lifetime of the process.
|
|
12
|
+
*/
|
|
13
|
+
const emitQueues = new Map<string, Promise<unknown>>();
|
|
14
|
+
|
|
15
|
+
export function queueEmitByOutput<T>(outputJsonPath: string, action: () => Promise<T>): Promise<T> {
|
|
16
|
+
const previous = emitQueues.get(outputJsonPath) ?? Promise.resolve();
|
|
17
|
+
// Continue regardless of the previous task's outcome — a failed emit must not
|
|
18
|
+
// block subsequent ones. The current task's outcome propagates via `next`.
|
|
19
|
+
const next = previous.then(action, action);
|
|
20
|
+
emitQueues.set(outputJsonPath, next);
|
|
21
|
+
return next;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function disposeEmitQueue(outputJsonPath: string): void {
|
|
25
|
+
emitQueues.delete(outputJsonPath);
|
|
26
|
+
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Maps MigrationGraph + status info to the generic graph renderer types.
|
|
3
3
|
*/
|
|
4
4
|
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
5
|
-
import { findPath } from '@prisma-next/migration-tools/dag';
|
|
6
5
|
import type { MigrationGraph } from '@prisma-next/migration-tools/graph';
|
|
6
|
+
import { findPath } from '@prisma-next/migration-tools/migration-graph';
|
|
7
7
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
8
8
|
|
|
9
9
|
import type { StatusRef } from '../migration-types';
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, join } from 'pathe';
|
|
3
|
+
|
|
4
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
5
|
+
return typeof value === 'object' && value !== null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function createTempArtifactPath(path: string, publicationToken: string, phase: string): string {
|
|
9
|
+
return join(dirname(path), `.${basename(path)}.${process.pid}.${publicationToken}.${phase}.tmp`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type PreviousArtifact = { readonly content: string } | 'remove';
|
|
13
|
+
|
|
14
|
+
async function readExistingArtifact(path: string): Promise<PreviousArtifact> {
|
|
15
|
+
try {
|
|
16
|
+
return { content: await readFile(path, 'utf-8') };
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (isRecord(error) && error['code'] === 'ENOENT') {
|
|
19
|
+
return 'remove';
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function restoreArtifact(
|
|
26
|
+
path: string,
|
|
27
|
+
previous: PreviousArtifact,
|
|
28
|
+
publicationToken: string,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
if (previous === 'remove') {
|
|
31
|
+
await rm(path, { force: true });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const restorePath = createTempArtifactPath(path, publicationToken, 'rollback');
|
|
36
|
+
await writeFile(restorePath, previous.content, 'utf-8');
|
|
37
|
+
try {
|
|
38
|
+
await rename(restorePath, path);
|
|
39
|
+
} finally {
|
|
40
|
+
await rm(restorePath, { force: true });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface PublishEntry {
|
|
45
|
+
readonly tempPath: string;
|
|
46
|
+
readonly outputPath: string;
|
|
47
|
+
readonly previous: PreviousArtifact;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function withRollbackFailureCause(error: unknown, rollbackFailures: readonly unknown[]): Error {
|
|
51
|
+
const rollbackCause = new AggregateError(
|
|
52
|
+
rollbackFailures,
|
|
53
|
+
'Failed to restore published artifacts',
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (error instanceof Error) {
|
|
57
|
+
Object.defineProperty(error, 'cause', {
|
|
58
|
+
value: rollbackCause,
|
|
59
|
+
configurable: true,
|
|
60
|
+
writable: true,
|
|
61
|
+
});
|
|
62
|
+
return error;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return new Error(String(error), { cause: rollbackCause });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function publishPairWithRollback(
|
|
69
|
+
entries: readonly PublishEntry[],
|
|
70
|
+
publicationToken: string,
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
const replaced: PublishEntry[] = [];
|
|
73
|
+
try {
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
await rename(entry.tempPath, entry.outputPath);
|
|
76
|
+
replaced.push(entry);
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
const rollbackResults = await Promise.allSettled(
|
|
80
|
+
replaced.map((entry) => restoreArtifact(entry.outputPath, entry.previous, publicationToken)),
|
|
81
|
+
);
|
|
82
|
+
const rollbackFailures = rollbackResults.flatMap((result) =>
|
|
83
|
+
result.status === 'rejected' ? [result.reason] : [],
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (rollbackFailures.length > 0) {
|
|
87
|
+
throw withRollbackFailureCause(error, rollbackFailures);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function publishContractArtifactPair({
|
|
95
|
+
outputJsonPath,
|
|
96
|
+
outputDtsPath,
|
|
97
|
+
contractJson,
|
|
98
|
+
contractDts,
|
|
99
|
+
publicationToken,
|
|
100
|
+
beforePublish,
|
|
101
|
+
}: {
|
|
102
|
+
readonly outputJsonPath: string;
|
|
103
|
+
readonly outputDtsPath: string;
|
|
104
|
+
readonly contractJson: string;
|
|
105
|
+
readonly contractDts: string;
|
|
106
|
+
readonly publicationToken: string;
|
|
107
|
+
readonly beforePublish?: () => Promise<boolean> | boolean;
|
|
108
|
+
}): Promise<boolean> {
|
|
109
|
+
const tempJsonPath = createTempArtifactPath(outputJsonPath, publicationToken, 'next');
|
|
110
|
+
const tempDtsPath = createTempArtifactPath(outputDtsPath, publicationToken, 'next');
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
await writeFile(tempJsonPath, contractJson, 'utf-8');
|
|
114
|
+
await writeFile(tempDtsPath, contractDts, 'utf-8');
|
|
115
|
+
|
|
116
|
+
if ((await beforePublish?.()) === false) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const previousJson = await readExistingArtifact(outputJsonPath);
|
|
121
|
+
const previousDts = await readExistingArtifact(outputDtsPath);
|
|
122
|
+
|
|
123
|
+
await publishPairWithRollback(
|
|
124
|
+
[
|
|
125
|
+
{ tempPath: tempDtsPath, outputPath: outputDtsPath, previous: previousDts },
|
|
126
|
+
{ tempPath: tempJsonPath, outputPath: outputJsonPath, previous: previousJson },
|
|
127
|
+
],
|
|
128
|
+
publicationToken,
|
|
129
|
+
);
|
|
130
|
+
return true;
|
|
131
|
+
} finally {
|
|
132
|
+
await Promise.allSettled([rm(tempJsonPath, { force: true }), rm(tempDtsPath, { force: true })]);
|
|
133
|
+
}
|
|
134
|
+
}
|