@prisma-next/cli 0.12.0-dev.2 → 0.12.0-dev.20
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/dist/cli.mjs +177 -160
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-KgJorIvG.mjs → client-Cdxcme1x.mjs} +21 -8
- package/dist/client-Cdxcme1x.mjs.map +1 -0
- package/dist/{command-helpers-Bbw1GbwL.mjs → command-helpers-Cmdqyhz9.mjs} +32 -2
- package/dist/{command-helpers-Bbw1GbwL.mjs.map → command-helpers-Cmdqyhz9.mjs.map} +1 -1
- package/dist/commands/contract-emit.mjs +1 -1
- package/dist/commands/contract-infer.mjs +1 -1
- package/dist/commands/db-init.mjs +4 -4
- package/dist/commands/db-schema.mjs +3 -3
- package/dist/commands/db-sign.mjs +4 -4
- package/dist/commands/db-update.mjs +5 -5
- package/dist/commands/db-verify.mjs +1 -1
- package/dist/commands/migrate.d.mts +1 -1
- package/dist/commands/migrate.mjs +5 -5
- package/dist/commands/migration-check.mjs +1 -1
- package/dist/commands/migration-graph.d.mts +23 -5
- package/dist/commands/migration-graph.d.mts.map +1 -1
- package/dist/commands/migration-graph.mjs +2 -2
- package/dist/commands/migration-list.d.mts +3 -3
- package/dist/commands/migration-list.mjs +3 -3
- package/dist/commands/migration-log.d.mts +3 -3
- package/dist/commands/migration-log.mjs +3 -3
- package/dist/commands/migration-new.mjs +3 -3
- package/dist/commands/migration-plan.d.mts +1 -1
- package/dist/commands/migration-plan.mjs +1 -1
- package/dist/commands/migration-show.d.mts +1 -1
- package/dist/commands/migration-show.mjs +3 -3
- package/dist/commands/migration-status.d.mts +1 -1
- package/dist/commands/migration-status.mjs +4 -4
- package/dist/commands/migration-status.mjs.map +1 -1
- package/dist/commands/ref.d.mts +1 -1
- package/dist/commands/ref.mjs +2 -2
- package/dist/commands/telemetry/index.d.mts +7 -0
- package/dist/commands/telemetry/index.d.mts.map +1 -0
- package/dist/commands/telemetry/index.mjs +2 -0
- package/dist/{contract-at-errors-BxP-TOMl.mjs → contract-at-errors-Cz0z5PJi.mjs} +2 -2
- package/dist/{contract-at-errors-BxP-TOMl.mjs.map → contract-at-errors-Cz0z5PJi.mjs.map} +1 -1
- package/dist/{contract-emit-D-4jrNve.mjs → contract-emit-CC9jDOmu.mjs} +3 -3
- package/dist/{contract-emit-D-4jrNve.mjs.map → contract-emit-CC9jDOmu.mjs.map} +1 -1
- package/dist/{contract-emit-DxcGl4Uq.mjs → contract-emit-DPMij44i.mjs} +3 -3
- package/dist/{contract-emit-DxcGl4Uq.mjs.map → contract-emit-DPMij44i.mjs.map} +1 -1
- package/dist/{contract-infer-D8uEbJuu.mjs → contract-infer-DaFPNrZH.mjs} +3 -3
- package/dist/{contract-infer-D8uEbJuu.mjs.map → contract-infer-DaFPNrZH.mjs.map} +1 -1
- package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs → contract-space-aggregate-loader-CirAEsM8.mjs} +2 -2
- package/dist/{contract-space-aggregate-loader-DvZwdkrr.mjs.map → contract-space-aggregate-loader-CirAEsM8.mjs.map} +1 -1
- package/dist/{db-verify-v_vUKXTU.mjs → db-verify-BSA1a_W_.mjs} +4 -4
- package/dist/{db-verify-v_vUKXTU.mjs.map → db-verify-BSA1a_W_.mjs.map} +1 -1
- package/dist/exports/control-api.d.mts +1 -1
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +2 -2
- package/dist/exports/index.mjs +1 -1
- package/dist/exports/init-output.mjs +1 -1
- package/dist/{framework-components-fYXjz_in.mjs → framework-components-DynSvww4.mjs} +2 -2
- package/dist/{framework-components-fYXjz_in.mjs.map → framework-components-DynSvww4.mjs.map} +1 -1
- package/dist/{global-flags-DEHjV8_s.d.mts → global-flags-DG4uY5tV.d.mts} +1 -1
- package/dist/{global-flags-DEHjV8_s.d.mts.map → global-flags-DG4uY5tV.d.mts.map} +1 -1
- package/dist/{init-Cv9UzWL5.mjs → init-B6kKrmf7.mjs} +5 -58
- package/dist/init-B6kKrmf7.mjs.map +1 -0
- package/dist/{inspect-live-schema-C6ohV_oQ.mjs → inspect-live-schema-Dn56wDhG.mjs} +3 -3
- package/dist/{inspect-live-schema-C6ohV_oQ.mjs.map → inspect-live-schema-Dn56wDhG.mjs.map} +1 -1
- package/dist/{migration-check-BiBJoYYW.mjs → migration-check-DzH1u-O1.mjs} +2 -2
- package/dist/{migration-check-BiBJoYYW.mjs.map → migration-check-DzH1u-O1.mjs.map} +1 -1
- package/dist/{migration-command-scaffold-CjvwO6at.mjs → migration-command-scaffold-V52dV2Tv.mjs} +3 -3
- package/dist/{migration-command-scaffold-CjvwO6at.mjs.map → migration-command-scaffold-V52dV2Tv.mjs.map} +1 -1
- package/dist/{migration-graph-D7DVUElV.mjs → migration-graph-DKl_IYsF.mjs} +377 -85
- package/dist/migration-graph-DKl_IYsF.mjs.map +1 -0
- package/dist/{migration-list-styler-BRwF4-gy.mjs → migration-list-styler-COQbZmXk.mjs} +61 -46
- package/dist/migration-list-styler-COQbZmXk.mjs.map +1 -0
- package/dist/{migration-plan-9DJ7q7_z.mjs → migration-plan-CaeKCKp4.mjs} +5 -5
- package/dist/{migration-plan-9DJ7q7_z.mjs.map → migration-plan-CaeKCKp4.mjs.map} +1 -1
- package/dist/{migration-types-D2FW63pr.d.mts → migration-types-CAQ-0TEE.d.mts} +1 -1
- package/dist/{migration-types-D2FW63pr.d.mts.map → migration-types-CAQ-0TEE.d.mts.map} +1 -1
- package/dist/{migrations-Cv2jxNNK.mjs → migrations-DQ1t3XFL.mjs} +2 -2
- package/dist/{migrations-Cv2jxNNK.mjs.map → migrations-DQ1t3XFL.mjs.map} +1 -1
- package/dist/{output-B60Gw5fu.mjs → output-CF_hqzI-.mjs} +1 -1
- package/dist/{output-B60Gw5fu.mjs.map → output-CF_hqzI-.mjs.map} +1 -1
- package/dist/telemetry-Q88WHwlv.mjs +122 -0
- package/dist/telemetry-Q88WHwlv.mjs.map +1 -0
- package/dist/{terminal-ui-5Y6mrg93.d.mts → terminal-ui-C3xGyxW-.d.mts} +1 -1
- package/dist/{terminal-ui-5Y6mrg93.d.mts.map → terminal-ui-C3xGyxW-.d.mts.map} +1 -1
- package/dist/{types-Dt_SfqFm.d.mts → types-DiC683UW.d.mts} +8 -2
- package/dist/{types-Dt_SfqFm.d.mts.map → types-DiC683UW.d.mts.map} +1 -1
- package/dist/{verify-DCA9Sldu.mjs → verify-CreSJ1Mz.mjs} +2 -2
- package/dist/{verify-DCA9Sldu.mjs.map → verify-CreSJ1Mz.mjs.map} +1 -1
- package/package.json +22 -18
- package/src/cli.ts +5 -0
- package/src/commands/init/index.ts +6 -35
- package/src/commands/init/init.ts +1 -14
- package/src/commands/init/inputs.ts +0 -75
- package/src/commands/migration-graph.ts +43 -2
- package/src/commands/migration-status.ts +1 -1
- package/src/commands/telemetry/index.ts +107 -0
- package/src/commands/telemetry/status.ts +67 -0
- package/src/control-api/client.ts +11 -1
- package/src/control-api/operations/apply.ts +1 -0
- package/src/control-api/operations/migration-apply.ts +10 -3
- package/src/control-api/types.ts +12 -1
- package/src/utils/formatters/migration-graph-lane-colors.ts +31 -0
- package/src/utils/formatters/migration-graph-layout.ts +51 -7
- package/src/utils/formatters/migration-graph-tree-render.ts +414 -51
- package/src/utils/formatters/migration-list-graph-topology.ts +67 -83
- package/src/utils/global-flags.ts +35 -0
- package/src/utils/telemetry.ts +68 -32
- package/dist/client-KgJorIvG.mjs.map +0 -1
- package/dist/init-Cv9UzWL5.mjs.map +0 -1
- package/dist/migration-graph-D7DVUElV.mjs.map +0 -1
- package/dist/migration-list-styler-BRwF4-gy.mjs.map +0 -1
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import * as clack from '@clack/prompts';
|
|
3
|
-
import { readUserConfig, resolveGating, writeUserConfig } from '@prisma-next/cli-telemetry';
|
|
4
3
|
import { extname, join, normalize } from 'pathe';
|
|
5
4
|
import type { GlobalFlags } from '../../utils/global-flags';
|
|
6
|
-
import { isCI } from '../../utils/is-ci';
|
|
7
5
|
import {
|
|
8
6
|
errorInitAuthoringSchemaPathMismatch,
|
|
9
7
|
errorInitInvalidFlagValue,
|
|
@@ -71,18 +69,6 @@ export interface ResolvedInitInputs {
|
|
|
71
69
|
* is added separately via the install step.
|
|
72
70
|
*/
|
|
73
71
|
readonly removePreviousFacade: string | null;
|
|
74
|
-
/**
|
|
75
|
-
* Telemetry consent answer recorded during this `init` run, or `null`
|
|
76
|
-
* when no prompt was shown. The prompt fires only on the
|
|
77
|
-
* canPrompt + !autoAcceptPrompts + no env/CI opt-out +
|
|
78
|
-
* `enableTelemetry === undefined` intersection; outside that window
|
|
79
|
-
* the field is `null` and the stored preference (if any) stays
|
|
80
|
-
* unchanged. The answer has already been persisted to
|
|
81
|
-
* `$XDG_CONFIG_HOME/prisma-next/config.json` by
|
|
82
|
-
* the time `runInit` sees this value — it's surfaced here purely so
|
|
83
|
-
* the post-init summary can mention what the user chose.
|
|
84
|
-
*/
|
|
85
|
-
readonly enableTelemetry: boolean | null;
|
|
86
72
|
/**
|
|
87
73
|
* Whether to run `npx skills add prisma/prisma-next#v<version>` at the
|
|
88
74
|
* project level after install + emit. True by default; `--no-skill`
|
|
@@ -106,12 +92,6 @@ const AUTHORING_VALUES: ReadonlyMap<string, AuthoringId> = new Map([
|
|
|
106
92
|
['ts', 'typescript'],
|
|
107
93
|
]);
|
|
108
94
|
|
|
109
|
-
export const TELEMETRY_CONSENT_MESSAGE = [
|
|
110
|
-
'Help us prioritize features by sharing anonymous CLI usage data?',
|
|
111
|
-
'The telemetry implementation is open source and fully transparent.',
|
|
112
|
-
'(packages/1-framework/3-tooling/cli-telemetry and apps/telemetry-backend).',
|
|
113
|
-
].join(' ');
|
|
114
|
-
|
|
115
95
|
/**
|
|
116
96
|
* Resolves every required input for `runInit`. In interactive mode, missing
|
|
117
97
|
* inputs are prompted via clack; in non-interactive mode, missing required
|
|
@@ -197,11 +177,6 @@ export async function resolveInitInputs(ctx: {
|
|
|
197
177
|
autoAcceptPrompts,
|
|
198
178
|
});
|
|
199
179
|
|
|
200
|
-
const enableTelemetry = await resolveTelemetryConsent({
|
|
201
|
-
canPrompt,
|
|
202
|
-
autoAcceptPrompts,
|
|
203
|
-
});
|
|
204
|
-
|
|
205
180
|
// Skill-install gating. `--no-skill` (commander parses
|
|
206
181
|
// `options.skill === false`) is the only escape hatch; otherwise
|
|
207
182
|
// project-level install is unconditional. The skill is always
|
|
@@ -219,60 +194,10 @@ export async function resolveInitInputs(ctx: {
|
|
|
219
194
|
strictProbe: Boolean(options.strictProbe),
|
|
220
195
|
reinit,
|
|
221
196
|
removePreviousFacade,
|
|
222
|
-
enableTelemetry,
|
|
223
197
|
installProjectSkill,
|
|
224
198
|
};
|
|
225
199
|
}
|
|
226
200
|
|
|
227
|
-
/**
|
|
228
|
-
* The interactive telemetry consent prompt. Shown as the last
|
|
229
|
-
* question of the `init` sequence iff:
|
|
230
|
-
* 1. `canPrompt === true` (interactive stdin / stdout combo),
|
|
231
|
-
* 2. `autoAcceptPrompts === false` (the user did not pass `--yes`),
|
|
232
|
-
* 3. neither telemetry env opt-out is active,
|
|
233
|
-
* 4. the process is not running in CI,
|
|
234
|
-
* 5. the stored `enableTelemetry` value is `undefined` (the user
|
|
235
|
-
* has never been asked, or skipped the prompt previously).
|
|
236
|
-
*
|
|
237
|
-
* Outside that intersection the function returns `null` and leaves the
|
|
238
|
-
* stored preference untouched. Inside it, the user's answer is
|
|
239
|
-
* persisted via `writeUserConfig({ enableTelemetry })` before this
|
|
240
|
-
* function returns; on an affirmative answer `writeUserConfig`
|
|
241
|
-
* generates and stores the v4 `installationId` in the same write.
|
|
242
|
-
*
|
|
243
|
-
* The wording names CLI usage data and points to the open-source
|
|
244
|
-
* client/backend paths. Default value is `true` to match precedent
|
|
245
|
-
* and to make the consent answer a single keystroke once it's been
|
|
246
|
-
* disclosed.
|
|
247
|
-
*/
|
|
248
|
-
async function resolveTelemetryConsent(opts: {
|
|
249
|
-
readonly canPrompt: boolean;
|
|
250
|
-
readonly autoAcceptPrompts: boolean;
|
|
251
|
-
}): Promise<boolean | null> {
|
|
252
|
-
if (!opts.canPrompt || opts.autoAcceptPrompts || isCI()) {
|
|
253
|
-
return null;
|
|
254
|
-
}
|
|
255
|
-
const config = readUserConfig();
|
|
256
|
-
const gating = resolveGating({ env: process.env, config });
|
|
257
|
-
if (!gating.enabled && gating.reason === 'env-override') {
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
const stored = config.enableTelemetry;
|
|
261
|
-
if (stored !== undefined) {
|
|
262
|
-
return null;
|
|
263
|
-
}
|
|
264
|
-
const result = await clack.confirm({
|
|
265
|
-
message: TELEMETRY_CONSENT_MESSAGE,
|
|
266
|
-
initialValue: true,
|
|
267
|
-
output: process.stderr,
|
|
268
|
-
});
|
|
269
|
-
if (clack.isCancel(result)) {
|
|
270
|
-
throw errorInitUserAborted();
|
|
271
|
-
}
|
|
272
|
-
writeUserConfig({ enableTelemetry: Boolean(result) });
|
|
273
|
-
return Boolean(result);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
201
|
async function resolveWriteEnv(opts: {
|
|
277
202
|
readonly flag: boolean | undefined;
|
|
278
203
|
readonly canPrompt: boolean;
|
|
@@ -16,7 +16,10 @@ import { migrationGraphToRenderInput } from '../utils/formatters/graph-migration
|
|
|
16
16
|
import { graphRenderer } from '../utils/formatters/graph-render';
|
|
17
17
|
import { buildMigrationGraphLayout } from '../utils/formatters/migration-graph-layout';
|
|
18
18
|
import { buildMigrationGraphRows } from '../utils/formatters/migration-graph-rows';
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
renderMigrationGraphLegend,
|
|
21
|
+
renderMigrationGraphTree,
|
|
22
|
+
} from '../utils/formatters/migration-graph-tree-render';
|
|
20
23
|
import { formatStyledHeader } from '../utils/formatters/styled';
|
|
21
24
|
import type { CommonCommandOptions } from '../utils/global-flags';
|
|
22
25
|
import { type GlobalFlags, parseGlobalFlagsOrExit } from '../utils/global-flags';
|
|
@@ -29,6 +32,32 @@ interface MigrationGraphOptions extends CommonCommandOptions {
|
|
|
29
32
|
readonly dot?: boolean;
|
|
30
33
|
readonly tree?: boolean;
|
|
31
34
|
readonly ascii?: boolean;
|
|
35
|
+
readonly legend?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `--legend` describes the `--tree` visual language, so passing it auto-enables
|
|
40
|
+
* the tree path (it has nothing to say about the legacy dagre default).
|
|
41
|
+
*/
|
|
42
|
+
export function migrationGraphUsesTree(options: {
|
|
43
|
+
readonly tree?: boolean;
|
|
44
|
+
readonly legend?: boolean;
|
|
45
|
+
}): boolean {
|
|
46
|
+
return options.tree === true || options.legend === true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The legend is decoration printed alongside the command header on stderr, so
|
|
51
|
+
* it is suppressed for the machine-readable / silent paths (`--json`, `--dot`,
|
|
52
|
+
* `--quiet`) exactly as the header is.
|
|
53
|
+
*/
|
|
54
|
+
export function migrationGraphShowsLegend(
|
|
55
|
+
options: { readonly legend?: boolean; readonly dot?: boolean },
|
|
56
|
+
flags: GlobalFlags,
|
|
57
|
+
): boolean {
|
|
58
|
+
return (
|
|
59
|
+
options.legend === true && options.dot !== true && flags.json !== true && flags.quiet !== true
|
|
60
|
+
);
|
|
32
61
|
}
|
|
33
62
|
|
|
34
63
|
export interface MigrationGraphResult {
|
|
@@ -61,6 +90,16 @@ export async function executeMigrationGraphCommand(
|
|
|
61
90
|
flags,
|
|
62
91
|
});
|
|
63
92
|
ui.stderr(header);
|
|
93
|
+
if (migrationGraphShowsLegend(options, flags)) {
|
|
94
|
+
ui.stderr(
|
|
95
|
+
renderMigrationGraphLegend({
|
|
96
|
+
colorize: flags.color !== false,
|
|
97
|
+
glyphMode: ui.resolveGlyphMode(options.ascii === true),
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
// Blank line separating the stderr key from the graph that follows on stdout.
|
|
101
|
+
ui.stderr('');
|
|
102
|
+
}
|
|
64
103
|
}
|
|
65
104
|
|
|
66
105
|
const loaded = await buildReadAggregate(config, { migrationsDir });
|
|
@@ -102,6 +141,7 @@ export function createMigrationGraphCommand(): Command {
|
|
|
102
141
|
'prisma-next migration graph --dot',
|
|
103
142
|
'prisma-next migration graph --tree',
|
|
104
143
|
'prisma-next migration graph --tree --ascii',
|
|
144
|
+
'prisma-next migration graph --legend',
|
|
105
145
|
]);
|
|
106
146
|
setCommandSeeAlso(command, [
|
|
107
147
|
{ verb: 'migration status', oneLiner: 'Show migration path and pending status' },
|
|
@@ -114,6 +154,7 @@ export function createMigrationGraphCommand(): Command {
|
|
|
114
154
|
.option('--dot', 'Output in Graphviz DOT format')
|
|
115
155
|
.option('--tree', 'Experimental condensed annotated tree renderer')
|
|
116
156
|
.option('--ascii', 'Use ASCII glyphs for --tree (pipe-friendly)')
|
|
157
|
+
.option('--legend', 'Print a key for the --tree glyphs and lane colors (implies --tree)')
|
|
117
158
|
.action(async (options: MigrationGraphOptions) => {
|
|
118
159
|
const flags = parseGlobalFlagsOrExit(options);
|
|
119
160
|
const ui = createTerminalUI(flags);
|
|
@@ -145,7 +186,7 @@ export function createMigrationGraphCommand(): Command {
|
|
|
145
186
|
JSON.stringify({ ok: true, nodes, edges, summary: graphResult.summary }, null, 2),
|
|
146
187
|
);
|
|
147
188
|
} else if (!flags.quiet) {
|
|
148
|
-
if (options
|
|
189
|
+
if (migrationGraphUsesTree(options)) {
|
|
149
190
|
const refsByHash = new Map<string, string[]>();
|
|
150
191
|
for (const ref of graphResult.refs) {
|
|
151
192
|
const existing = refsByHash.get(ref.hash);
|
|
@@ -500,7 +500,7 @@ export async function loadAggregateStatusSpaces(args: {
|
|
|
500
500
|
// Count pending *migrations* (graph edges), not operations: a
|
|
501
501
|
// single authored migration that lowers to N ops or zero ops
|
|
502
502
|
// both count as exactly one pending unit of work for the user.
|
|
503
|
-
pendingCount = walked.result.migrationEdges
|
|
503
|
+
pendingCount = walked.result.migrationEdges.length;
|
|
504
504
|
if (liveMarker === null) {
|
|
505
505
|
status = pendingCount === 0 ? 'no-marker' : 'pending';
|
|
506
506
|
} else {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { userConfigPath, writeUserConfig } from '@prisma-next/cli-telemetry';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import {
|
|
4
|
+
addGlobalOptions,
|
|
5
|
+
setCommandDescriptions,
|
|
6
|
+
setCommandExamples,
|
|
7
|
+
} from '../../utils/command-helpers';
|
|
8
|
+
import { formatCommandHelp } from '../../utils/formatters/help';
|
|
9
|
+
import {
|
|
10
|
+
type CommonCommandOptions,
|
|
11
|
+
parseGlobalFlags,
|
|
12
|
+
parseGlobalFlagsOrExit,
|
|
13
|
+
} from '../../utils/global-flags';
|
|
14
|
+
import { isCI } from '../../utils/is-ci';
|
|
15
|
+
import { createTerminalUI } from '../../utils/terminal-ui';
|
|
16
|
+
import { formatTelemetryStatusLines, resolveTelemetryStatus } from './status';
|
|
17
|
+
|
|
18
|
+
function createTelemetryStatusCommand(): Command {
|
|
19
|
+
const command = new Command('status');
|
|
20
|
+
setCommandDescriptions(
|
|
21
|
+
command,
|
|
22
|
+
'Show whether anonymous CLI telemetry is enabled and why',
|
|
23
|
+
'Reports whether telemetry is currently enabled or disabled and the reason\n' +
|
|
24
|
+
'(default-on, stored opt-out, environment opt-out, or CI), the path to your\n' +
|
|
25
|
+
'user-level config file, and whether an installation ID has been stored.\n' +
|
|
26
|
+
'Read-only: never sends an event, never mints an ID, never writes anything.',
|
|
27
|
+
);
|
|
28
|
+
return addGlobalOptions(command).action((options: CommonCommandOptions) => {
|
|
29
|
+
const flags = parseGlobalFlagsOrExit(options);
|
|
30
|
+
const ui = createTerminalUI(flags);
|
|
31
|
+
const status = resolveTelemetryStatus({ env: process.env, inCI: isCI() });
|
|
32
|
+
if (flags.json) {
|
|
33
|
+
ui.output(JSON.stringify(status));
|
|
34
|
+
} else {
|
|
35
|
+
for (const line of formatTelemetryStatusLines(status)) {
|
|
36
|
+
ui.output(line);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
process.exit(0);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createTelemetryEnableCommand(): Command {
|
|
44
|
+
const command = new Command('enable');
|
|
45
|
+
setCommandDescriptions(
|
|
46
|
+
command,
|
|
47
|
+
'Enable anonymous CLI telemetry',
|
|
48
|
+
'Stores "enableTelemetry": true in your user-level config and mints an\n' +
|
|
49
|
+
'installation ID if one is not already stored.',
|
|
50
|
+
);
|
|
51
|
+
return addGlobalOptions(command).action((options: CommonCommandOptions) => {
|
|
52
|
+
const flags = parseGlobalFlagsOrExit(options);
|
|
53
|
+
writeUserConfig({ enableTelemetry: true });
|
|
54
|
+
const ui = createTerminalUI(flags);
|
|
55
|
+
if (flags.json) {
|
|
56
|
+
ui.output(JSON.stringify({ enableTelemetry: true, configPath: userConfigPath() }));
|
|
57
|
+
} else {
|
|
58
|
+
ui.output(`Telemetry enabled. Preference stored in ${userConfigPath()}.`);
|
|
59
|
+
}
|
|
60
|
+
process.exit(0);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function createTelemetryDisableCommand(): Command {
|
|
65
|
+
const command = new Command('disable');
|
|
66
|
+
setCommandDescriptions(
|
|
67
|
+
command,
|
|
68
|
+
'Disable anonymous CLI telemetry',
|
|
69
|
+
'Stores "enableTelemetry": false in your user-level config. No installation\n' +
|
|
70
|
+
'ID is minted and no event is sent.',
|
|
71
|
+
);
|
|
72
|
+
return addGlobalOptions(command).action((options: CommonCommandOptions) => {
|
|
73
|
+
const flags = parseGlobalFlagsOrExit(options);
|
|
74
|
+
writeUserConfig({ enableTelemetry: false });
|
|
75
|
+
const ui = createTerminalUI(flags);
|
|
76
|
+
if (flags.json) {
|
|
77
|
+
ui.output(JSON.stringify({ enableTelemetry: false, configPath: userConfigPath() }));
|
|
78
|
+
} else {
|
|
79
|
+
ui.output(`Telemetry disabled. Preference stored in ${userConfigPath()}.`);
|
|
80
|
+
}
|
|
81
|
+
process.exit(0);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createTelemetryCommand(): Command {
|
|
86
|
+
const command = new Command('telemetry');
|
|
87
|
+
setCommandDescriptions(
|
|
88
|
+
command,
|
|
89
|
+
'Inspect and change anonymous CLI telemetry',
|
|
90
|
+
'Show telemetry status, or enable / disable anonymous CLI usage data.\n' +
|
|
91
|
+
'Telemetry is on by default (opt-out); see https://prisma-next.dev/docs/cli/telemetry\n' +
|
|
92
|
+
'for what is collected and why.',
|
|
93
|
+
);
|
|
94
|
+
setCommandExamples(command, [
|
|
95
|
+
'prisma-next telemetry status',
|
|
96
|
+
'prisma-next telemetry disable',
|
|
97
|
+
'prisma-next telemetry enable',
|
|
98
|
+
]);
|
|
99
|
+
command.configureHelp({
|
|
100
|
+
formatHelp: (cmd) => formatCommandHelp({ command: cmd, flags: parseGlobalFlags({}) }),
|
|
101
|
+
subcommandDescription: () => '',
|
|
102
|
+
});
|
|
103
|
+
command.addCommand(createTelemetryStatusCommand());
|
|
104
|
+
command.addCommand(createTelemetryEnableCommand());
|
|
105
|
+
command.addCommand(createTelemetryDisableCommand());
|
|
106
|
+
return command;
|
|
107
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readUserConfig, resolveGating, userConfigPath } from '@prisma-next/cli-telemetry';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Why telemetry resolves the way it does, in the order the CLI's
|
|
5
|
+
* `resolveTelemetryGate` evaluates: CI hard-disables first, then the env
|
|
6
|
+
* opt-outs, then the stored `enableTelemetry`, then the opt-out default.
|
|
7
|
+
*/
|
|
8
|
+
export type TelemetryStatusReason =
|
|
9
|
+
| 'ci'
|
|
10
|
+
| 'env-opt-out'
|
|
11
|
+
| 'stored-opt-out'
|
|
12
|
+
| 'stored-opt-in'
|
|
13
|
+
| 'default-on';
|
|
14
|
+
|
|
15
|
+
export interface TelemetryStatus {
|
|
16
|
+
readonly enabled: boolean;
|
|
17
|
+
readonly reason: TelemetryStatusReason;
|
|
18
|
+
readonly configPath: string;
|
|
19
|
+
readonly installationIdStored: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolves the same gate the runtime uses (CI check + `resolveGating`) and
|
|
24
|
+
* projects it into a user-facing status. Pure read: never mints, never
|
|
25
|
+
* writes. The `installationId` value itself is never surfaced — only its
|
|
26
|
+
* presence — so `status` discloses nothing identifying.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveTelemetryStatus(inputs: {
|
|
29
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
30
|
+
readonly inCI: boolean;
|
|
31
|
+
}): TelemetryStatus {
|
|
32
|
+
const config = readUserConfig();
|
|
33
|
+
const configPath = userConfigPath();
|
|
34
|
+
const installationIdStored =
|
|
35
|
+
typeof config.installationId === 'string' && config.installationId.length > 0;
|
|
36
|
+
|
|
37
|
+
if (inputs.inCI) {
|
|
38
|
+
return { enabled: false, reason: 'ci', configPath, installationIdStored };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const gating = resolveGating({ env: inputs.env, config });
|
|
42
|
+
if (!gating.enabled) {
|
|
43
|
+
const reason: TelemetryStatusReason =
|
|
44
|
+
gating.reason === 'env-override' ? 'env-opt-out' : 'stored-opt-out';
|
|
45
|
+
return { enabled: false, reason, configPath, installationIdStored };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const reason: TelemetryStatusReason =
|
|
49
|
+
config.enableTelemetry === true ? 'stored-opt-in' : 'default-on';
|
|
50
|
+
return { enabled: true, reason, configPath, installationIdStored };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const REASON_EXPLANATION: Record<TelemetryStatusReason, string> = {
|
|
54
|
+
ci: 'CI environment detected — telemetry is hard-disabled.',
|
|
55
|
+
'env-opt-out': 'an environment opt-out is set (DO_NOT_TRACK / PRISMA_NEXT_DISABLE_TELEMETRY).',
|
|
56
|
+
'stored-opt-out': '"enableTelemetry": false is stored in your config.',
|
|
57
|
+
'stored-opt-in': '"enableTelemetry": true is stored in your config.',
|
|
58
|
+
'default-on': 'no explicit choice is stored, so the opt-out default applies.',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export function formatTelemetryStatusLines(status: TelemetryStatus): string[] {
|
|
62
|
+
return [
|
|
63
|
+
`Telemetry is ${status.enabled ? 'enabled' : 'disabled'}: ${REASON_EXPLANATION[status.reason]}`,
|
|
64
|
+
`Config file: ${status.configPath}`,
|
|
65
|
+
`Installation ID: ${status.installationIdStored ? 'stored' : 'not stored'}`,
|
|
66
|
+
];
|
|
67
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Contract,
|
|
3
|
+
ContractMarkerRecord,
|
|
4
|
+
LedgerEntryRecord,
|
|
5
|
+
} from '@prisma-next/contract/types';
|
|
2
6
|
import { emit as emitContractArtifacts } from '@prisma-next/emitter';
|
|
3
7
|
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
4
8
|
import type {
|
|
@@ -447,6 +451,12 @@ class ControlClientImpl implements ControlClient {
|
|
|
447
451
|
return familyInstance.readAllMarkers({ driver });
|
|
448
452
|
}
|
|
449
453
|
|
|
454
|
+
/** Reads the per-migration journal for `space` (defaults to the app contract space). */
|
|
455
|
+
async readLedger(space = APP_SPACE_ID): Promise<readonly LedgerEntryRecord[]> {
|
|
456
|
+
const { driver, familyInstance } = await this.ensureConnected();
|
|
457
|
+
return familyInstance.readLedger({ driver, space });
|
|
458
|
+
}
|
|
459
|
+
|
|
450
460
|
async migrationApply(options: MigrationApplyOptions): Promise<MigrationApplyResult> {
|
|
451
461
|
const { onProgress } = options;
|
|
452
462
|
await this.connectWithProgress(options.connection, 'migrationApply', onProgress);
|
|
@@ -141,6 +141,7 @@ export async function applyMigration<TFamilyId extends string, TTargetId extends
|
|
|
141
141
|
destinationContract: r.entry.destinationContract,
|
|
142
142
|
policy,
|
|
143
143
|
frameworkComponents,
|
|
144
|
+
migrationEdges: r.entry.migrationEdges,
|
|
144
145
|
// Per-space post-apply schema verification is non-strict: each
|
|
145
146
|
// space's `destinationContract` describes only its own slice; a
|
|
146
147
|
// strict verifier would treat every other space's tables as
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
TargetMigrationsCapability,
|
|
8
8
|
} from '@prisma-next/framework-components/control';
|
|
9
9
|
import {
|
|
10
|
+
buildSynthMigrationEdge,
|
|
10
11
|
type ContractMarkerRecordLike,
|
|
11
12
|
type ContractSpaceAggregate,
|
|
12
13
|
type ContractSpaceMember,
|
|
@@ -319,7 +320,7 @@ export async function executeMigrationApply<TFamilyId extends string, TTargetId
|
|
|
319
320
|
includeMarkers: true,
|
|
320
321
|
});
|
|
321
322
|
const totalMigrationsApplied = applied.value.orderedResolutions.reduce(
|
|
322
|
-
(sum, r) => sum +
|
|
323
|
+
(sum, r) => sum + r.entry.migrationEdges.length,
|
|
323
324
|
0,
|
|
324
325
|
);
|
|
325
326
|
const summary = `Applied ${totalMigrationsApplied} migration(s) (${applied.value.totalOpsExecuted} operation(s)) across ${orderedAll.length} contract space(s)`;
|
|
@@ -361,7 +362,13 @@ function buildAtHeadResolution(args: {
|
|
|
361
362
|
displayOps: [],
|
|
362
363
|
destinationContract: member.contract(),
|
|
363
364
|
strategy: 'graph-walk',
|
|
364
|
-
migrationEdges: [
|
|
365
|
+
migrationEdges: [
|
|
366
|
+
buildSynthMigrationEdge({
|
|
367
|
+
currentMarkerStorageHash: liveMarker?.storageHash,
|
|
368
|
+
destinationStorageHash: targetHash,
|
|
369
|
+
operationCount: 0,
|
|
370
|
+
}),
|
|
371
|
+
],
|
|
365
372
|
};
|
|
366
373
|
}
|
|
367
374
|
|
|
@@ -404,7 +411,7 @@ function buildSuccess(args: BuildSuccessArgs): MigrationApplySuccess {
|
|
|
404
411
|
// JSON-shape consumers (e.g. `parsed.applied.length` in integration
|
|
405
412
|
// tests). The aggregate per-space breakdown lives on `perSpace[]`.
|
|
406
413
|
const applied = args.orderedResolutions.flatMap((r) => {
|
|
407
|
-
const edges = r.entry.migrationEdges
|
|
414
|
+
const edges = r.entry.migrationEdges;
|
|
408
415
|
return edges.map((edge) => ({
|
|
409
416
|
spaceId: r.spaceId,
|
|
410
417
|
dirName: edge.dirName,
|
package/src/control-api/types.ts
CHANGED
|
@@ -2,7 +2,11 @@ import type {
|
|
|
2
2
|
ContractSourceDiagnostics,
|
|
3
3
|
ContractSourceProvider,
|
|
4
4
|
} from '@prisma-next/config/config-types';
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
Contract,
|
|
7
|
+
ContractMarkerRecord,
|
|
8
|
+
LedgerEntryRecord,
|
|
9
|
+
} from '@prisma-next/contract/types';
|
|
6
10
|
import type {
|
|
7
11
|
ControlAdapterDescriptor,
|
|
8
12
|
ControlDriverDescriptor,
|
|
@@ -876,6 +880,13 @@ export interface ControlClient {
|
|
|
876
880
|
*/
|
|
877
881
|
readAllMarkers(): Promise<ReadonlyMap<string, ContractMarkerRecord>>;
|
|
878
882
|
|
|
883
|
+
/**
|
|
884
|
+
* Reads the per-migration ledger journal for `space` in apply order.
|
|
885
|
+
* Returns an empty array when the ledger store does not yet exist or
|
|
886
|
+
* has no rows for that space.
|
|
887
|
+
*/
|
|
888
|
+
readLedger(space?: string): Promise<readonly LedgerEntryRecord[]>;
|
|
889
|
+
|
|
879
890
|
/**
|
|
880
891
|
* Applies pre-planned on-disk migrations to the database.
|
|
881
892
|
* Each migration runs in its own transaction with full execution checks.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createColors } from 'colorette';
|
|
2
|
+
|
|
3
|
+
export type LaneColorizer = (text: string) => string;
|
|
4
|
+
|
|
5
|
+
const { magenta, cyan, green, yellow, blueBright, red } = createColors({ useColor: true });
|
|
6
|
+
|
|
7
|
+
export const LANE_COLOR_CYCLE: readonly LaneColorizer[] = [
|
|
8
|
+
magenta,
|
|
9
|
+
cyan,
|
|
10
|
+
green,
|
|
11
|
+
yellow,
|
|
12
|
+
blueBright,
|
|
13
|
+
red,
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The hue for a gutter column. The leftmost lane (column 0) is **neutral** — it
|
|
18
|
+
* has nothing to be told apart from in the common single-lane linear case, so
|
|
19
|
+
* the renderer dims it rather than tinting it; the rotating palette is reserved
|
|
20
|
+
* for columns ≥ 1 (where a second lane exists to distinguish). Callers must dim
|
|
21
|
+
* column 0 themselves; this returns identity for it as a guard. A lane freed and
|
|
22
|
+
* reused by a later branch keeps its column's hue — coloring is by position, not
|
|
23
|
+
* branch identity, exactly like `git log --graph`.
|
|
24
|
+
*/
|
|
25
|
+
export function laneColorForColumn(column: number): LaneColorizer {
|
|
26
|
+
if (column <= 0) {
|
|
27
|
+
return (text) => text;
|
|
28
|
+
}
|
|
29
|
+
const colorizer = LANE_COLOR_CYCLE[(column - 1) % LANE_COLOR_CYCLE.length];
|
|
30
|
+
return colorizer ?? ((text) => text);
|
|
31
|
+
}
|
|
@@ -21,6 +21,7 @@ export type StructuralCell =
|
|
|
21
21
|
| { readonly kind: 'arc-branch-corner' }
|
|
22
22
|
| { readonly kind: 'arc-branch-tee' }
|
|
23
23
|
| { readonly kind: 'arc-land-corner' }
|
|
24
|
+
| { readonly kind: 'arc-land-tee' }
|
|
24
25
|
| { readonly kind: 'arc-crossing' }
|
|
25
26
|
| { readonly kind: 'arc-land-bridge' }
|
|
26
27
|
| {
|
|
@@ -379,6 +380,7 @@ function emptyCells(width: number): StructuralCell[] {
|
|
|
379
380
|
function buildBranchConnectorCells(
|
|
380
381
|
startLane: number,
|
|
381
382
|
endLane: number,
|
|
383
|
+
fanTargetLanes: ReadonlySet<number>,
|
|
382
384
|
activeLanes: ReadonlySet<number>,
|
|
383
385
|
gridWidth: number,
|
|
384
386
|
): StructuralCell[] {
|
|
@@ -393,7 +395,13 @@ function buildBranchConnectorCells(
|
|
|
393
395
|
} else if (lane === endLane) {
|
|
394
396
|
cells[lane] = { kind: 'branch-corner' };
|
|
395
397
|
} else if (lane > startLane && lane < endLane) {
|
|
396
|
-
|
|
398
|
+
if (fanTargetLanes.has(lane)) {
|
|
399
|
+
cells[lane] = { kind: 'branch-tee' };
|
|
400
|
+
} else if (activeLanes.has(lane)) {
|
|
401
|
+
cells[lane] = { kind: 'arc-crossing' };
|
|
402
|
+
} else {
|
|
403
|
+
cells[lane] = { kind: 'branch-tee' };
|
|
404
|
+
}
|
|
397
405
|
}
|
|
398
406
|
}
|
|
399
407
|
return cells;
|
|
@@ -402,6 +410,7 @@ function buildBranchConnectorCells(
|
|
|
402
410
|
function buildMergeConnectorCells(
|
|
403
411
|
startLane: number,
|
|
404
412
|
endLane: number,
|
|
413
|
+
fanTargetLanes: ReadonlySet<number>,
|
|
405
414
|
activeLanes: ReadonlySet<number>,
|
|
406
415
|
gridWidth: number,
|
|
407
416
|
): StructuralCell[] {
|
|
@@ -416,7 +425,13 @@ function buildMergeConnectorCells(
|
|
|
416
425
|
} else if (lane === endLane) {
|
|
417
426
|
cells[lane] = { kind: 'merge-corner' };
|
|
418
427
|
} else if (lane > startLane && lane < endLane) {
|
|
419
|
-
|
|
428
|
+
if (fanTargetLanes.has(lane)) {
|
|
429
|
+
cells[lane] = { kind: 'merge-tee' };
|
|
430
|
+
} else if (activeLanes.has(lane)) {
|
|
431
|
+
cells[lane] = { kind: 'arc-crossing' };
|
|
432
|
+
} else {
|
|
433
|
+
cells[lane] = { kind: 'horizontal-pass' };
|
|
434
|
+
}
|
|
420
435
|
}
|
|
421
436
|
}
|
|
422
437
|
return cells;
|
|
@@ -689,6 +704,15 @@ function applySkipRollbackRouting(
|
|
|
689
704
|
.map((other) => other.backLane);
|
|
690
705
|
const maxCoSourcedLane = Math.max(...coSourcedLanes);
|
|
691
706
|
|
|
707
|
+
// Back-lanes of arcs that converge on this same target node. They share the
|
|
708
|
+
// node's landing row, so each inner lane reads as a `┴` junction (the outer
|
|
709
|
+
// arcs' horizontal bridge passes through it on the way to the node) and only
|
|
710
|
+
// the outermost closes the corner with `╯`.
|
|
711
|
+
const coLandingLanes = routes
|
|
712
|
+
.filter((other) => other.edge.to === edge.to)
|
|
713
|
+
.map((other) => other.backLane);
|
|
714
|
+
const maxCoLandingLane = Math.max(...coLandingLanes);
|
|
715
|
+
|
|
692
716
|
const sourceRow = result[sourceRowIndex];
|
|
693
717
|
if (sourceRow !== undefined) {
|
|
694
718
|
const cells = sourceRow.cells;
|
|
@@ -750,6 +774,7 @@ function applySkipRollbackRouting(
|
|
|
750
774
|
const existing = cells[backLane];
|
|
751
775
|
if (
|
|
752
776
|
existing?.kind !== 'arc-land-corner' &&
|
|
777
|
+
existing?.kind !== 'arc-land-tee' &&
|
|
753
778
|
existing?.kind !== 'arc-land-bridge' &&
|
|
754
779
|
existing?.kind !== 'arc-branch-corner' &&
|
|
755
780
|
existing?.kind !== 'arc-branch-tee' &&
|
|
@@ -766,6 +791,12 @@ function applySkipRollbackRouting(
|
|
|
766
791
|
const contractHash = targetRow.contractHash ?? EMPTY_CONTRACT_HASH;
|
|
767
792
|
cells[targetCol] = { kind: 'node', contractHash, arcLand: true };
|
|
768
793
|
for (let lane = targetCol + 1; lane < backLane; lane += 1) {
|
|
794
|
+
// An inner converging arc's own landing junction: the outer arcs' bridge
|
|
795
|
+
// passes through it (`┴`) while its own vertical run closes here.
|
|
796
|
+
if (coLandingLanes.includes(lane)) {
|
|
797
|
+
cells[lane] = { kind: 'arc-land-tee' };
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
769
800
|
// A bridged lane that carries another arc OR a forward vertical still
|
|
770
801
|
// active at this row must cross over it (`┼`) rather than overwrite it
|
|
771
802
|
// with a bare bridge (`──`).
|
|
@@ -774,7 +805,8 @@ function applySkipRollbackRouting(
|
|
|
774
805
|
existing !== undefined &&
|
|
775
806
|
existing.kind !== 'empty' &&
|
|
776
807
|
existing.kind !== 'horizontal-pass' &&
|
|
777
|
-
existing.kind !== 'arc-land-bridge'
|
|
808
|
+
existing.kind !== 'arc-land-bridge' &&
|
|
809
|
+
existing.kind !== 'arc-land-tee';
|
|
778
810
|
const crossed =
|
|
779
811
|
occupied ||
|
|
780
812
|
routes.some(
|
|
@@ -785,7 +817,10 @@ function applySkipRollbackRouting(
|
|
|
785
817
|
);
|
|
786
818
|
cells[lane] = crossed ? { kind: 'arc-crossing' } : { kind: 'arc-land-bridge' };
|
|
787
819
|
}
|
|
788
|
-
|
|
820
|
+
// Inner converging arcs close as a landing tee so the outermost arc's
|
|
821
|
+
// bridge reads through to the node; only the outermost arc draws `╯`.
|
|
822
|
+
cells[backLane] =
|
|
823
|
+
backLane < maxCoLandingLane ? { kind: 'arc-land-tee' } : { kind: 'arc-land-corner' };
|
|
789
824
|
for (const other of routes) {
|
|
790
825
|
if (other.backLane <= backLane) continue;
|
|
791
826
|
if (!routeCrossesRow(other, targetRowIndex, result)) continue;
|
|
@@ -793,6 +828,7 @@ function applySkipRollbackRouting(
|
|
|
793
828
|
const existing = cells[other.backLane];
|
|
794
829
|
if (
|
|
795
830
|
existing?.kind !== 'arc-land-corner' &&
|
|
831
|
+
existing?.kind !== 'arc-land-tee' &&
|
|
796
832
|
existing?.kind !== 'arc-land-bridge' &&
|
|
797
833
|
existing?.kind !== 'node'
|
|
798
834
|
) {
|
|
@@ -922,13 +958,14 @@ function layoutComponent(
|
|
|
922
958
|
const endLane = Math.max(...laneIndices);
|
|
923
959
|
ensureGridWidth(endLane + 1);
|
|
924
960
|
const activeLanes = new Set(activeLaneIndices());
|
|
961
|
+
const fanTargetLanes = new Set(laneIndices);
|
|
925
962
|
rows.push({
|
|
926
963
|
kind: 'merge-connector',
|
|
927
964
|
contractHash,
|
|
928
965
|
startLane,
|
|
929
966
|
endLane,
|
|
930
967
|
branchCount: laneIndices.length,
|
|
931
|
-
cells: buildMergeConnectorCells(startLane, endLane, activeLanes, gridWidth),
|
|
968
|
+
cells: buildMergeConnectorCells(startLane, endLane, fanTargetLanes, activeLanes, gridWidth),
|
|
932
969
|
});
|
|
933
970
|
for (const index of laneIndices) {
|
|
934
971
|
if (index !== startLane) setLane(index, null);
|
|
@@ -941,6 +978,7 @@ function layoutComponent(
|
|
|
941
978
|
startLane: number,
|
|
942
979
|
endLane: number,
|
|
943
980
|
branchCount: number,
|
|
981
|
+
fanTargetLanes: readonly number[],
|
|
944
982
|
): void {
|
|
945
983
|
ensureGridWidth(endLane + 1);
|
|
946
984
|
const activeLanes = new Set(activeLaneIndices());
|
|
@@ -950,7 +988,13 @@ function layoutComponent(
|
|
|
950
988
|
startLane,
|
|
951
989
|
endLane,
|
|
952
990
|
branchCount,
|
|
953
|
-
cells: buildBranchConnectorCells(
|
|
991
|
+
cells: buildBranchConnectorCells(
|
|
992
|
+
startLane,
|
|
993
|
+
endLane,
|
|
994
|
+
new Set(fanTargetLanes),
|
|
995
|
+
activeLanes,
|
|
996
|
+
gridWidth,
|
|
997
|
+
),
|
|
954
998
|
});
|
|
955
999
|
}
|
|
956
1000
|
|
|
@@ -1046,7 +1090,7 @@ function layoutComponent(
|
|
|
1046
1090
|
|
|
1047
1091
|
if (groups.length >= 2) {
|
|
1048
1092
|
const endLane = Math.max(...laneForGroup);
|
|
1049
|
-
emitBranchConnector(node, column, endLane, groups.length);
|
|
1093
|
+
emitBranchConnector(node, column, endLane, groups.length, laneForGroup);
|
|
1050
1094
|
}
|
|
1051
1095
|
|
|
1052
1096
|
for (let groupIndex = 0; groupIndex < groups.length; groupIndex++) {
|