@prisma-next/cli 0.12.0-dev.6 → 0.12.0-dev.8
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 +2 -2
- package/dist/commands/migration-graph.d.mts +19 -1
- package/dist/commands/migration-graph.d.mts.map +1 -1
- package/dist/commands/migration-graph.mjs +2 -2
- package/dist/{migration-graph-D7DVUElV.mjs → migration-graph-C9WC-7eO.mjs} +324 -78
- package/dist/migration-graph-C9WC-7eO.mjs.map +1 -0
- package/package.json +18 -18
- package/src/commands/migration-graph.ts +43 -2
- package/src/utils/formatters/migration-graph-lane-colors.ts +31 -0
- package/src/utils/formatters/migration-graph-layout.ts +27 -5
- package/src/utils/formatters/migration-graph-tree-render.ts +360 -51
- package/dist/migration-graph-D7DVUElV.mjs.map +0 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { EMPTY_CONTRACT_HASH } from '@prisma-next/migration-tools/constants';
|
|
2
|
-
import { bold } from 'colorette';
|
|
2
|
+
import { bold, createColors } from 'colorette';
|
|
3
3
|
import stringWidth from 'string-width';
|
|
4
4
|
import type { GlyphMode } from '../glyph-mode';
|
|
5
|
+
import { laneColorForColumn } from './migration-graph-lane-colors';
|
|
5
6
|
import type {
|
|
6
7
|
MigrationGraphGridModel,
|
|
7
8
|
MigrationGraphGridRow,
|
|
@@ -116,56 +117,276 @@ function arrowForEdgeKind(
|
|
|
116
117
|
return palette.edgeArrow[kind];
|
|
117
118
|
}
|
|
118
119
|
|
|
120
|
+
/**
|
|
121
|
+
* The leftmost lane (column 0) renders with the neutral dim lane style rather
|
|
122
|
+
* than a palette hue — in the common single-lane case it has nothing to be told
|
|
123
|
+
* apart from. Used as the "no owning arc" sentinel during colour resolution.
|
|
124
|
+
*/
|
|
125
|
+
const NEUTRAL_LANE = 0;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Forced bold for branch-coloured names. A branched name pairs its lane hue
|
|
129
|
+
* (also forced, via {@link laneColorForColumn}) with bold; both must emit even
|
|
130
|
+
* when colorette's ambient TTY detection is off, so the colorized branch name
|
|
131
|
+
* is deterministically bold + hue rather than hue-only.
|
|
132
|
+
*/
|
|
133
|
+
const { bold: forcedBold } = createColors({ useColor: true });
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The colour-source column for each cell of a row, resolved together because a
|
|
137
|
+
* routed back-arc spans columns and must read as **one hue** rather than a
|
|
138
|
+
* per-column "rainbow". An arc's horizontal bridges, corners, and node-pair
|
|
139
|
+
* connector all take the arc's owning back-lane column (the corner that closes
|
|
140
|
+
* the arc), not the column they pass through.
|
|
141
|
+
*/
|
|
142
|
+
interface RowLaneColors {
|
|
143
|
+
/** Colour column for a cell's structural glyph (lane / spine / arc body). */
|
|
144
|
+
readonly lane: readonly number[];
|
|
145
|
+
/** Colour column for a node arc-pair's connector half (`◂` / `─`). */
|
|
146
|
+
readonly connector: readonly number[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Resolve per-cell colour columns for a row. Scanning right-to-left lets each
|
|
151
|
+
* arc bridge inherit the corner column that closes it (the arc's back-lane), so
|
|
152
|
+
* the whole arc — vertical run (already its own column), horizontal bridges,
|
|
153
|
+
* corners, crossings, and the `◂`/`─` connector — reads as a single continuous
|
|
154
|
+
* hue. A crossing can only be one colour, so rather than leave it dim (wrong for
|
|
155
|
+
* both crossing lines) it takes the arc owning the horizontal run at this row
|
|
156
|
+
* (the nearest corner to its right); the crossed vertical lane is simply
|
|
157
|
+
* occluded at that one cell and reappears on the next row.
|
|
158
|
+
*/
|
|
159
|
+
function resolveRowLaneColors(cells: readonly StructuralCell[]): RowLaneColors {
|
|
160
|
+
const lane = new Array<number>(cells.length);
|
|
161
|
+
const connector = new Array<number>(cells.length);
|
|
162
|
+
let arcCorner = NEUTRAL_LANE;
|
|
163
|
+
for (let column = cells.length - 1; column >= 0; column--) {
|
|
164
|
+
const cell = cells[column];
|
|
165
|
+
connector[column] = arcCorner;
|
|
166
|
+
switch (cell?.kind) {
|
|
167
|
+
case 'arc-branch-corner':
|
|
168
|
+
case 'arc-land-corner':
|
|
169
|
+
arcCorner = column;
|
|
170
|
+
lane[column] = column;
|
|
171
|
+
break;
|
|
172
|
+
case 'arc-branch-tee':
|
|
173
|
+
// An inner co-sourced arc's own back-lane junction: its vertical run
|
|
174
|
+
// continues below in this column, so it keeps its own column hue.
|
|
175
|
+
lane[column] = column;
|
|
176
|
+
break;
|
|
177
|
+
case 'arc-crossing':
|
|
178
|
+
case 'arc-land-bridge':
|
|
179
|
+
lane[column] = arcCorner;
|
|
180
|
+
break;
|
|
181
|
+
case 'horizontal-pass':
|
|
182
|
+
lane[column] = arcCorner === NEUTRAL_LANE ? column : arcCorner;
|
|
183
|
+
break;
|
|
184
|
+
case 'node':
|
|
185
|
+
lane[column] = column;
|
|
186
|
+
arcCorner = NEUTRAL_LANE;
|
|
187
|
+
break;
|
|
188
|
+
default:
|
|
189
|
+
lane[column] = column;
|
|
190
|
+
arcCorner = NEUTRAL_LANE;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { lane, connector };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Per-cell colour for a forward branch/merge connector row, split into the
|
|
198
|
+
* cell's junction `glyph` and its trailing `dash`. A connector's horizontal run
|
|
199
|
+
* is one logical line (a fork into new lanes, or a merge into a surviving lane)
|
|
200
|
+
* and reads best as the colour of the lane each segment serves — not dim-gray
|
|
201
|
+
* or a per-pass-through-column "rainbow".
|
|
202
|
+
*/
|
|
203
|
+
interface ConnectorLaneColors {
|
|
204
|
+
/** Colour column for a cell's junction glyph (`├` / `┬` / `┴` / `╮` / `╯`). */
|
|
205
|
+
readonly glyph: readonly number[];
|
|
206
|
+
/** Colour column for a tee's trailing `─` — the branch it leads into. */
|
|
207
|
+
readonly dash: readonly number[];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Resolve per-cell connector colours. Scanning right-to-left, a corner or an
|
|
212
|
+
* intermediate tee anchors its own lane (its junction glyph takes that column),
|
|
213
|
+
* but a tee's **trailing dash leads into the branch on its right** (the next
|
|
214
|
+
* branch point), so `┬─` reads as "this lane, then on toward the next" rather
|
|
215
|
+
* than tinting the dash with the left lane. The leading tee at `startLane` (the
|
|
216
|
+
* fork/merge origin) and pure horizontal segments inherit the nearest branch
|
|
217
|
+
* point to their right whole-cell, so the run into a branch — or collapsing
|
|
218
|
+
* into a merge corner — stays continuous. Pass-through verticals outside the
|
|
219
|
+
* run keep their own column (column 0 stays neutral).
|
|
220
|
+
*/
|
|
221
|
+
function resolveConnectorLaneColors(
|
|
222
|
+
cells: readonly StructuralCell[],
|
|
223
|
+
startLane: number,
|
|
224
|
+
): ConnectorLaneColors {
|
|
225
|
+
const glyph = new Array<number>(cells.length);
|
|
226
|
+
const dash = new Array<number>(cells.length);
|
|
227
|
+
let owner = NEUTRAL_LANE;
|
|
228
|
+
for (let column = cells.length - 1; column >= 0; column--) {
|
|
229
|
+
const cell = cells[column];
|
|
230
|
+
switch (cell?.kind) {
|
|
231
|
+
case 'branch-corner':
|
|
232
|
+
case 'merge-corner':
|
|
233
|
+
owner = column;
|
|
234
|
+
glyph[column] = column;
|
|
235
|
+
dash[column] = column;
|
|
236
|
+
break;
|
|
237
|
+
case 'branch-tee':
|
|
238
|
+
case 'merge-tee':
|
|
239
|
+
if (column === startLane) {
|
|
240
|
+
const served = owner === NEUTRAL_LANE ? column : owner;
|
|
241
|
+
glyph[column] = column;
|
|
242
|
+
dash[column] = served;
|
|
243
|
+
} else {
|
|
244
|
+
dash[column] = owner === NEUTRAL_LANE ? column : owner;
|
|
245
|
+
glyph[column] = column;
|
|
246
|
+
owner = column;
|
|
247
|
+
}
|
|
248
|
+
break;
|
|
249
|
+
case 'arc-crossing':
|
|
250
|
+
glyph[column] = column;
|
|
251
|
+
dash[column] = column;
|
|
252
|
+
break;
|
|
253
|
+
case 'horizontal-pass': {
|
|
254
|
+
const served = owner === NEUTRAL_LANE ? column : owner;
|
|
255
|
+
glyph[column] = served;
|
|
256
|
+
dash[column] = served;
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
default:
|
|
260
|
+
glyph[column] = column;
|
|
261
|
+
dash[column] = column;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { glyph, dash };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Style a structural glyph by its resolved colour column. Column 0 and the
|
|
269
|
+
* neutral sentinel render dim (`style.lane`); columns ≥ 1 take a palette hue.
|
|
270
|
+
*/
|
|
271
|
+
function laneStylerForColumn(
|
|
272
|
+
colorColumn: number,
|
|
273
|
+
colorize: boolean,
|
|
274
|
+
style: MigrationListStyler,
|
|
275
|
+
): (text: string) => string {
|
|
276
|
+
if (!colorize || colorColumn <= NEUTRAL_LANE) {
|
|
277
|
+
return (text) => style.lane(text);
|
|
278
|
+
}
|
|
279
|
+
return laneColorForColumn(colorColumn);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Tint a branch-owned token (direction arrow, migration name) by its edge's
|
|
284
|
+
* lane so the whole branch row reads in one colour. Column 0 has nothing to be
|
|
285
|
+
* told apart from in the common linear chain, so it keeps the token's existing
|
|
286
|
+
* default styling (`fallback`) rather than a palette hue; only lanes ≥ 1 take a
|
|
287
|
+
* colour. With colour off, the fallback (also colourless) is used unchanged.
|
|
288
|
+
*/
|
|
289
|
+
function branchStylerOrDefault(
|
|
290
|
+
column: number,
|
|
291
|
+
colorize: boolean,
|
|
292
|
+
fallback: (text: string) => string,
|
|
293
|
+
): (text: string) => string {
|
|
294
|
+
if (!colorize || column <= NEUTRAL_LANE) {
|
|
295
|
+
return fallback;
|
|
296
|
+
}
|
|
297
|
+
return laneColorForColumn(column);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Render a connector tee (`├─` / `┬─` / `┴─`) with its junction glyph and its
|
|
302
|
+
* trailing dash coloured independently: the junction anchors its own lane while
|
|
303
|
+
* the dash leads into the branch on its right.
|
|
304
|
+
*/
|
|
305
|
+
function renderConnectorTee(
|
|
306
|
+
pair: string,
|
|
307
|
+
glyphColumn: number,
|
|
308
|
+
dashColumn: number,
|
|
309
|
+
colorize: boolean,
|
|
310
|
+
style: MigrationListStyler,
|
|
311
|
+
): string {
|
|
312
|
+
const glyph = laneStylerForColumn(glyphColumn, colorize, style);
|
|
313
|
+
if (glyphColumn === dashColumn) {
|
|
314
|
+
return glyph(pair);
|
|
315
|
+
}
|
|
316
|
+
return glyph(pair.slice(0, 1)) + laneStylerForColumn(dashColumn, colorize, style)(pair.slice(1));
|
|
317
|
+
}
|
|
318
|
+
|
|
119
319
|
/**
|
|
120
320
|
* A node-marker glyph pair (`○◂`, `○─`, `*<`, `*-`) is the contract node
|
|
121
|
-
* marker (`○` / `*`) followed by an arc connector (`◂` / `─` / `<` / `-`).
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
321
|
+
* marker (`○` / `*`) followed by an arc connector (`◂` / `─` / `<` / `-`). The
|
|
322
|
+
* marker takes its own lane's hue (so each node visibly belongs to its branch);
|
|
323
|
+
* the connector follows the arc it belongs to (its owning back-lane hue).
|
|
324
|
+
* Direction arrows are handled elsewhere — they take their edge's lane hue too.
|
|
125
325
|
*/
|
|
126
|
-
function renderNodeMarkerPair(
|
|
127
|
-
|
|
326
|
+
function renderNodeMarkerPair(
|
|
327
|
+
pair: string,
|
|
328
|
+
nodeColumn: number,
|
|
329
|
+
arcColumn: number,
|
|
330
|
+
colorize: boolean,
|
|
331
|
+
style: MigrationListStyler,
|
|
332
|
+
): string {
|
|
333
|
+
const marker = laneStylerForColumn(nodeColumn, colorize, style);
|
|
334
|
+
const connector = laneStylerForColumn(arcColumn, colorize, style);
|
|
335
|
+
return marker(pair.slice(0, 1)) + connector(pair.slice(1));
|
|
128
336
|
}
|
|
129
337
|
|
|
130
338
|
function renderCellPair(
|
|
131
339
|
cell: StructuralCell,
|
|
340
|
+
column: number,
|
|
341
|
+
colors: RowLaneColors,
|
|
342
|
+
colorize: boolean,
|
|
132
343
|
style: MigrationListStyler,
|
|
133
344
|
palette: MigrationGraphTreeGlyphPalette,
|
|
134
345
|
): string {
|
|
346
|
+
const laneColumn = colors.lane[column] ?? column;
|
|
347
|
+
const lane = laneStylerForColumn(laneColumn, colorize, style);
|
|
135
348
|
switch (cell.kind) {
|
|
136
|
-
case 'node':
|
|
137
|
-
|
|
138
|
-
if (cell.
|
|
139
|
-
|
|
349
|
+
case 'node': {
|
|
350
|
+
const arcColumn = colors.connector[column] ?? NEUTRAL_LANE;
|
|
351
|
+
if (cell.arcLand === true) {
|
|
352
|
+
return renderNodeMarkerPair(palette.arcLand, column, arcColumn, colorize, style);
|
|
353
|
+
}
|
|
354
|
+
if (cell.arcTee === true) {
|
|
355
|
+
return renderNodeMarkerPair(palette.arcTee, column, arcColumn, colorize, style);
|
|
356
|
+
}
|
|
357
|
+
return lane(palette.node);
|
|
358
|
+
}
|
|
140
359
|
case 'vertical-pass':
|
|
141
|
-
return
|
|
360
|
+
return lane(palette.verticalPass);
|
|
142
361
|
case 'edge-lane':
|
|
143
|
-
// The lane stays dim; the direction arrow (↑ / ↓ / ⟲) is the signal and
|
|
144
|
-
// stays bright, like the contract-node marker.
|
|
145
362
|
return cell.ownsLabel
|
|
146
|
-
?
|
|
147
|
-
|
|
148
|
-
|
|
363
|
+
? lane(palette.verticalPass.trimEnd()) +
|
|
364
|
+
branchStylerOrDefault(
|
|
365
|
+
column,
|
|
366
|
+
colorize,
|
|
367
|
+
style.kind,
|
|
368
|
+
)(arrowForEdgeKind(cell.edgeKind, palette))
|
|
369
|
+
: lane(palette.verticalPass);
|
|
149
370
|
case 'branch-tee':
|
|
150
|
-
return
|
|
371
|
+
return lane(palette.branchTee);
|
|
151
372
|
case 'merge-tee':
|
|
152
|
-
return
|
|
373
|
+
return lane(palette.mergeTee);
|
|
153
374
|
case 'branch-corner':
|
|
154
|
-
return
|
|
375
|
+
return lane(palette.branchCorner);
|
|
155
376
|
case 'merge-corner':
|
|
156
|
-
return
|
|
377
|
+
return lane(palette.mergeCorner);
|
|
157
378
|
case 'arc-branch-corner':
|
|
158
|
-
return
|
|
379
|
+
return lane(palette.arcBranchCorner);
|
|
159
380
|
case 'arc-branch-tee':
|
|
160
|
-
return
|
|
381
|
+
return lane(palette.arcBranchTee);
|
|
161
382
|
case 'arc-land-corner':
|
|
162
|
-
return
|
|
383
|
+
return lane(palette.arcLandCorner);
|
|
163
384
|
case 'arc-crossing':
|
|
164
|
-
return
|
|
385
|
+
return lane(palette.arcLandBridge);
|
|
165
386
|
case 'arc-land-bridge':
|
|
166
|
-
return
|
|
387
|
+
return lane(palette.arcLandBridge);
|
|
167
388
|
case 'horizontal-pass':
|
|
168
|
-
return
|
|
389
|
+
return lane(palette.horizontalPass);
|
|
169
390
|
case 'empty':
|
|
170
391
|
return ' ';
|
|
171
392
|
}
|
|
@@ -174,34 +395,56 @@ function renderCellPair(
|
|
|
174
395
|
function renderConnectorRow(
|
|
175
396
|
row: MigrationGraphGridRow,
|
|
176
397
|
gridWidth: number,
|
|
398
|
+
colorize: boolean,
|
|
177
399
|
style: MigrationListStyler,
|
|
178
400
|
palette: MigrationGraphTreeGlyphPalette,
|
|
179
401
|
): string {
|
|
180
402
|
const isMerge = row.kind === 'merge-connector';
|
|
181
403
|
if (row.cells.length > 0) {
|
|
404
|
+
const colors = resolveConnectorLaneColors(row.cells, row.startLane ?? 0);
|
|
182
405
|
let seenTee = false;
|
|
183
406
|
let out = '';
|
|
184
|
-
for (
|
|
407
|
+
for (let column = 0; column < row.cells.length; column++) {
|
|
408
|
+
const cell = row.cells[column];
|
|
409
|
+
if (cell === undefined) continue;
|
|
410
|
+
const glyphColumn = colors.glyph[column] ?? column;
|
|
411
|
+
const dashColumn = colors.dash[column] ?? glyphColumn;
|
|
412
|
+
const lane = laneStylerForColumn(glyphColumn, colorize, style);
|
|
185
413
|
switch (cell.kind) {
|
|
186
414
|
case 'branch-tee':
|
|
187
|
-
out +=
|
|
415
|
+
out += renderConnectorTee(
|
|
416
|
+
seenTee ? palette.connectorBranchTeeCo : palette.connectorBranchTee,
|
|
417
|
+
glyphColumn,
|
|
418
|
+
dashColumn,
|
|
419
|
+
colorize,
|
|
420
|
+
style,
|
|
421
|
+
);
|
|
188
422
|
seenTee = true;
|
|
189
423
|
break;
|
|
190
424
|
case 'merge-tee':
|
|
191
|
-
out +=
|
|
425
|
+
out += renderConnectorTee(
|
|
426
|
+
seenTee ? palette.connectorMergeTeeCo : palette.connectorBranchTee,
|
|
427
|
+
glyphColumn,
|
|
428
|
+
dashColumn,
|
|
429
|
+
colorize,
|
|
430
|
+
style,
|
|
431
|
+
);
|
|
192
432
|
seenTee = true;
|
|
193
433
|
break;
|
|
194
434
|
case 'branch-corner':
|
|
195
|
-
out +=
|
|
435
|
+
out += lane(palette.branchCorner);
|
|
196
436
|
break;
|
|
197
437
|
case 'merge-corner':
|
|
198
|
-
out +=
|
|
438
|
+
out += lane(palette.mergeCorner);
|
|
199
439
|
break;
|
|
200
440
|
case 'vertical-pass':
|
|
201
|
-
out +=
|
|
441
|
+
out += lane(palette.verticalPass);
|
|
202
442
|
break;
|
|
203
443
|
case 'horizontal-pass':
|
|
204
|
-
out +=
|
|
444
|
+
out += lane(palette.horizontalPass);
|
|
445
|
+
break;
|
|
446
|
+
case 'arc-crossing':
|
|
447
|
+
out += renderConnectorTee(palette.arcCrossing, glyphColumn, dashColumn, colorize, style);
|
|
205
448
|
break;
|
|
206
449
|
default:
|
|
207
450
|
out += ' ';
|
|
@@ -218,13 +461,15 @@ function renderConnectorRow(
|
|
|
218
461
|
|
|
219
462
|
const start = row.startLane ?? 0;
|
|
220
463
|
const end = row.endLane ?? start;
|
|
464
|
+
// The whole fork/merge run reads as one line in the served lane's hue (the
|
|
465
|
+
// corner it reaches); pass-through columns outside the run keep their own.
|
|
466
|
+
const runLane = laneStylerForColumn(end, colorize, style);
|
|
221
467
|
let out = '';
|
|
222
468
|
for (let column = 0; column < gridWidth; column++) {
|
|
223
469
|
if (column < start || column > end) out += ' ';
|
|
224
|
-
else if (column === start) out +=
|
|
225
|
-
else if (column === end)
|
|
226
|
-
|
|
227
|
-
else out += style.lane(isMerge ? palette.connectorMergeTeeCo : palette.connectorBranchTeeCo);
|
|
470
|
+
else if (column === start) out += runLane(palette.connectorBranchTee);
|
|
471
|
+
else if (column === end) out += runLane(isMerge ? palette.mergeCorner : palette.branchCorner);
|
|
472
|
+
else out += runLane(isMerge ? palette.connectorMergeTeeCo : palette.connectorBranchTeeCo);
|
|
228
473
|
}
|
|
229
474
|
return out;
|
|
230
475
|
}
|
|
@@ -297,6 +542,13 @@ function padVisible(text: string, targetWidth: number): string {
|
|
|
297
542
|
return text + ' '.repeat(padding);
|
|
298
543
|
}
|
|
299
544
|
|
|
545
|
+
const ANSI_ESCAPE = '\x1b';
|
|
546
|
+
|
|
547
|
+
function trimTrailingWhitespace(line: string): string {
|
|
548
|
+
const trailingSpaceBeforeReset = new RegExp(`[\\t ]+((?:${ANSI_ESCAPE}\\[[0-9;]*m)+)$`);
|
|
549
|
+
return line.replace(trailingSpaceBeforeReset, '$1').replace(/\s+$/, '');
|
|
550
|
+
}
|
|
551
|
+
|
|
300
552
|
function gridWidthForModel(rows: readonly MigrationGraphGridRow[]): number {
|
|
301
553
|
return rows.reduce(
|
|
302
554
|
(max, row) =>
|
|
@@ -373,19 +625,32 @@ export function renderMigrationGraphTree(
|
|
|
373
625
|
}
|
|
374
626
|
|
|
375
627
|
if (row.kind === 'branch-connector' || row.kind === 'merge-connector') {
|
|
376
|
-
lines.push(
|
|
628
|
+
lines.push(
|
|
629
|
+
trimTrailingWhitespace(renderConnectorRow(row, gridWidth, opts.colorize, style, palette)),
|
|
630
|
+
);
|
|
377
631
|
continue;
|
|
378
632
|
}
|
|
379
633
|
|
|
380
|
-
|
|
381
|
-
|
|
634
|
+
const cellColors = resolveRowLaneColors(row.cells);
|
|
635
|
+
let gutter = row.cells
|
|
636
|
+
.map((cell, column) =>
|
|
637
|
+
renderCellPair(cell, column, cellColors, opts.colorize, style, palette),
|
|
638
|
+
)
|
|
639
|
+
.join('');
|
|
382
640
|
let laneSpan = row.cells.length;
|
|
383
641
|
if (row.kind === 'node') {
|
|
384
642
|
const contractHash = row.contractHash ?? EMPTY_CONTRACT_HASH;
|
|
385
|
-
if (
|
|
643
|
+
if (contractHash === EMPTY_CONTRACT_HASH) {
|
|
386
644
|
laneSpan = 1;
|
|
387
645
|
} else {
|
|
388
|
-
|
|
646
|
+
let lastActiveColumn = -1;
|
|
647
|
+
for (let column = row.cells.length - 1; column >= 0; column--) {
|
|
648
|
+
if (row.cells[column]?.kind !== 'empty') {
|
|
649
|
+
lastActiveColumn = column;
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
laneSpan = lastActiveColumn >= 0 ? lastActiveColumn + 1 : 1;
|
|
389
654
|
}
|
|
390
655
|
}
|
|
391
656
|
const labelColumn =
|
|
@@ -402,12 +667,16 @@ export function renderMigrationGraphTree(
|
|
|
402
667
|
) {
|
|
403
668
|
gutter = row.cells
|
|
404
669
|
.slice(0, 1)
|
|
405
|
-
.map((cell) =>
|
|
670
|
+
.map((cell, column) =>
|
|
671
|
+
renderCellPair(cell, column, cellColors, opts.colorize, style, palette),
|
|
672
|
+
)
|
|
406
673
|
.join('');
|
|
407
674
|
} else if (row.kind === 'node' && laneSpan < row.cells.length && !nodeHasArcDecoration(row)) {
|
|
408
675
|
gutter = row.cells
|
|
409
676
|
.slice(0, laneSpan)
|
|
410
|
-
.map((cell) =>
|
|
677
|
+
.map((cell, column) =>
|
|
678
|
+
renderCellPair(cell, column, cellColors, opts.colorize, style, palette),
|
|
679
|
+
)
|
|
411
680
|
.join('');
|
|
412
681
|
} else if (gutter.length < laneSpan * 2) {
|
|
413
682
|
gutter = gutter.padEnd(laneSpan * 2, ' ');
|
|
@@ -421,16 +690,18 @@ export function renderMigrationGraphTree(
|
|
|
421
690
|
if (contractHash === EMPTY_CONTRACT_HASH) {
|
|
422
691
|
const trailingLanes = row.cells
|
|
423
692
|
.slice(1)
|
|
424
|
-
.map((cell) =>
|
|
693
|
+
.map((cell, offset) =>
|
|
694
|
+
renderCellPair(cell, offset + 1, cellColors, opts.colorize, style, palette),
|
|
695
|
+
)
|
|
425
696
|
.join('');
|
|
426
697
|
const emptyGutter = palette.emptySource.padEnd(2, ' ') + trailingLanes;
|
|
427
698
|
const overlayNames = overlayNamesForContract(contractHash, opts);
|
|
428
699
|
if (overlayNames.length === 0) {
|
|
429
|
-
lines.push(emptyGutter
|
|
700
|
+
lines.push(trimTrailingWhitespace(emptyGutter));
|
|
430
701
|
continue;
|
|
431
702
|
}
|
|
432
703
|
const overlay = style.refs(overlayNames);
|
|
433
|
-
lines.push(`${padVisible(emptyGutter, dataColumn)}${overlay}
|
|
704
|
+
lines.push(trimTrailingWhitespace(`${padVisible(emptyGutter, dataColumn)}${overlay}`));
|
|
434
705
|
continue;
|
|
435
706
|
}
|
|
436
707
|
const hashText = style.sourceHash(
|
|
@@ -442,7 +713,7 @@ export function renderMigrationGraphTree(
|
|
|
442
713
|
? ' '.repeat(Math.max(0, dataColumn - labelColumn - stringWidth(hashText)))
|
|
443
714
|
: '';
|
|
444
715
|
const overlay = overlayNames.length > 0 ? style.refs(overlayNames) : '';
|
|
445
|
-
lines.push(`${gutterPad}${hashText}${overlayPad}${overlay}
|
|
716
|
+
lines.push(trimTrailingWhitespace(`${gutterPad}${hashText}${overlayPad}${overlay}`));
|
|
446
717
|
continue;
|
|
447
718
|
}
|
|
448
719
|
|
|
@@ -450,10 +721,48 @@ export function renderMigrationGraphTree(
|
|
|
450
721
|
if (edge === undefined) continue;
|
|
451
722
|
|
|
452
723
|
const dirNamePadding = ' '.repeat(Math.max(0, dirNameWidth - edge.dirName.length));
|
|
453
|
-
const
|
|
724
|
+
const laneIndex = row.laneIndex ?? 0;
|
|
725
|
+
// A branched name keeps its bold (via `style.dirName`) and adds the lane
|
|
726
|
+
// hue, so it reads as one with its lane/arrow; column-0 names stay bold-only.
|
|
727
|
+
const dirNameStyler =
|
|
728
|
+
opts.colorize && laneIndex > NEUTRAL_LANE
|
|
729
|
+
? (text: string) => forcedBold(laneColorForColumn(laneIndex)(text))
|
|
730
|
+
: style.dirName;
|
|
731
|
+
const dirName = `${dirNameStyler(edge.dirName)}${dirNamePadding}`;
|
|
454
732
|
const hashColumn = formatEdgeHashColumn(edge, style, hashLength, palette);
|
|
455
|
-
lines.push(`${gutterPad}${dirName}${hashColumn}
|
|
733
|
+
lines.push(trimTrailingWhitespace(`${gutterPad}${dirName}${hashColumn}`));
|
|
456
734
|
}
|
|
457
735
|
|
|
458
736
|
return lines.join('\n');
|
|
459
737
|
}
|
|
738
|
+
|
|
739
|
+
export interface RenderMigrationGraphLegendOptions {
|
|
740
|
+
readonly colorize: boolean;
|
|
741
|
+
readonly glyphMode?: GlyphMode;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* A compact key for the `--tree` visual language: the contract marker, the
|
|
746
|
+
* in-lane direction arrows, the empty baseline, the `(refs)` overlay (including
|
|
747
|
+
* the reserved `db` live-database and `contract` working-schema markers), and a
|
|
748
|
+
* worked sample of the data-column `from → to` migration hash arrow.
|
|
749
|
+
*
|
|
750
|
+
* Honors the same glyph palette (unicode vs ASCII) and `colorize` gate as the
|
|
751
|
+
* tree renderer, so the key matches whatever the graph itself drew and stays
|
|
752
|
+
* pipe-safe (zero ANSI when color is off). The caller adds the trailing blank
|
|
753
|
+
* line that separates this stderr key from the graph on stdout.
|
|
754
|
+
*/
|
|
755
|
+
export function renderMigrationGraphLegend(opts: RenderMigrationGraphLegendOptions): string {
|
|
756
|
+
const palette = paletteFor(opts.glyphMode ?? 'unicode');
|
|
757
|
+
const style = createAnsiMigrationListStyler({ useColor: opts.colorize });
|
|
758
|
+
const node = palette.node.trimEnd();
|
|
759
|
+
const sampleArrow = `${style.sourceHash('aaaaaa')} ${style.glyph(palette.forwardArrow)} ${style.destHash('bbbbbb')}`;
|
|
760
|
+
return [
|
|
761
|
+
'Legend:',
|
|
762
|
+
` ${style.kind(node)} contract ${style.kind(palette.edgeArrow.forward)} forward ${style.kind(palette.edgeArrow.rollback)} rollback`,
|
|
763
|
+
` ${style.kind(palette.edgeArrow.self)} migration without schema change`,
|
|
764
|
+
` ${style.glyph(palette.emptySource)} empty database (baseline)`,
|
|
765
|
+
` ${style.refs(['refs'])} ${DB_MARKER_NAME} / ${CONTRACT_MARKER_NAME} markers`,
|
|
766
|
+
` ${sampleArrow} migration from contract aaaaaa to bbbbbb`,
|
|
767
|
+
].join('\n');
|
|
768
|
+
}
|