@pgpmjs/core 7.3.0 → 7.5.0

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/slice/slice.d.ts DELETED
@@ -1,59 +0,0 @@
1
- import { Change, Tag, ExtendedPlanFile } from '@pgpmjs/ast/files/types';
2
- import { SliceConfig, SliceResult, DependencyGraph, PackageOutput, GroupingStrategy, PatternStrategy, CrossPackageDepMode } from './types';
3
- /**
4
- * Build a dependency graph from a parsed plan file
5
- */
6
- export declare function buildDependencyGraph(plan: ExtendedPlanFile): DependencyGraph;
7
- /**
8
- * Validate that the dependency graph is a DAG (no cycles)
9
- */
10
- export declare function validateDAG(graph: DependencyGraph): void;
11
- /**
12
- * Extract package name from a change path using folder-based strategy
13
- */
14
- export declare function extractPackageFromPath(changeName: string, depth?: number, prefixToStrip?: string): string;
15
- /**
16
- * Find the matching package for a change using pattern-based strategy.
17
- * Returns the first matching slice's package name, or undefined if no match.
18
- */
19
- export declare function findMatchingPattern(changeName: string, strategy: PatternStrategy): string | undefined;
20
- /**
21
- * Assign changes to packages based on grouping strategy
22
- */
23
- export declare function assignChangesToPackages(graph: DependencyGraph, strategy: GroupingStrategy, defaultPackage?: string): Map<string, Set<string>>;
24
- /**
25
- * Merge small packages into larger ones
26
- */
27
- export declare function mergeSmallPackages(assignments: Map<string, Set<string>>, minChanges: number, defaultPackage?: string): Map<string, Set<string>>;
28
- /**
29
- * Build package-level dependency graph
30
- */
31
- export declare function buildPackageDependencies(graph: DependencyGraph, assignments: Map<string, Set<string>>): Map<string, Set<string>>;
32
- /**
33
- * Detect cycles in package dependency graph
34
- */
35
- export declare function detectPackageCycle(deps: Map<string, Set<string>>): string[] | null;
36
- /**
37
- * Compute deployment order for packages (topological sort)
38
- */
39
- export declare function computeDeployOrder(packageDeps: Map<string, Set<string>>): string[];
40
- /**
41
- * Topological sort of changes within a package
42
- */
43
- export declare function topologicalSortWithinPackage(changes: Set<string>, graph: DependencyGraph): string[];
44
- /**
45
- * Generate plan file content for a package
46
- */
47
- export declare function generatePlanContent(pkgName: string, entries: Change[], tags?: Tag[]): string;
48
- /**
49
- * Generate control file content for a package
50
- */
51
- export declare function generateControlContent(pkgName: string, deps: Set<string>): string;
52
- /**
53
- * Generate a single package output
54
- */
55
- export declare function generateSinglePackage(pkgName: string, changes: Set<string>, graph: DependencyGraph, changeToPackage: Map<string, string>, pkgDeps: Set<string>, crossPackageDeps?: boolean | CrossPackageDepMode): PackageOutput;
56
- /**
57
- * Main slicing function
58
- */
59
- export declare function slicePlan(config: SliceConfig): SliceResult;
package/slice/slice.js DELETED
@@ -1,519 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.buildDependencyGraph = buildDependencyGraph;
4
- exports.validateDAG = validateDAG;
5
- exports.extractPackageFromPath = extractPackageFromPath;
6
- exports.findMatchingPattern = findMatchingPattern;
7
- exports.assignChangesToPackages = assignChangesToPackages;
8
- exports.mergeSmallPackages = mergeSmallPackages;
9
- exports.buildPackageDependencies = buildPackageDependencies;
10
- exports.detectPackageCycle = detectPackageCycle;
11
- exports.computeDeployOrder = computeDeployOrder;
12
- exports.topologicalSortWithinPackage = topologicalSortWithinPackage;
13
- exports.generatePlanContent = generatePlanContent;
14
- exports.generateControlContent = generateControlContent;
15
- exports.generateSinglePackage = generateSinglePackage;
16
- exports.slicePlan = slicePlan;
17
- const parser_1 = require("@pgpmjs/ast/files/plan/parser");
18
- const writer_1 = require("@pgpmjs/ast/files/plan/writer");
19
- const minimatch_1 = require("minimatch");
20
- /**
21
- * Build a dependency graph from a parsed plan file
22
- */
23
- function buildDependencyGraph(plan) {
24
- const graph = {
25
- nodes: new Map(),
26
- edges: new Map(),
27
- reverseEdges: new Map(),
28
- tags: new Map(),
29
- plan
30
- };
31
- // Add all changes as nodes
32
- for (const change of plan.changes) {
33
- graph.nodes.set(change.name, change);
34
- graph.edges.set(change.name, new Set(change.dependencies || []));
35
- // Build reverse edges for dependency analysis
36
- for (const dep of change.dependencies || []) {
37
- if (!graph.reverseEdges.has(dep)) {
38
- graph.reverseEdges.set(dep, new Set());
39
- }
40
- graph.reverseEdges.get(dep).add(change.name);
41
- }
42
- }
43
- // Index tags by change
44
- for (const tag of plan.tags || []) {
45
- if (!graph.tags.has(tag.change)) {
46
- graph.tags.set(tag.change, []);
47
- }
48
- graph.tags.get(tag.change).push(tag);
49
- }
50
- return graph;
51
- }
52
- /**
53
- * Validate that the dependency graph is a DAG (no cycles)
54
- */
55
- function validateDAG(graph) {
56
- const visited = new Set();
57
- const visiting = new Set();
58
- function dfs(node, path) {
59
- if (visiting.has(node)) {
60
- throw new Error(`Cycle detected: ${[...path, node].join(' -> ')}`);
61
- }
62
- if (visited.has(node))
63
- return;
64
- visiting.add(node);
65
- const deps = graph.edges.get(node) || new Set();
66
- for (const dep of deps) {
67
- // Only follow internal dependencies
68
- if (graph.nodes.has(dep)) {
69
- dfs(dep, [...path, node]);
70
- }
71
- }
72
- visiting.delete(node);
73
- visited.add(node);
74
- }
75
- for (const node of graph.nodes.keys()) {
76
- dfs(node, []);
77
- }
78
- }
79
- /**
80
- * Extract package name from a change path using folder-based strategy
81
- */
82
- function extractPackageFromPath(changeName, depth = 1, prefixToStrip = 'schemas') {
83
- const parts = changeName.split('/');
84
- // Handle special folders that should go to core
85
- if (parts[0] === 'extensions' || parts[0] === 'migrate') {
86
- return 'core';
87
- }
88
- // Strip prefix if present
89
- let startIdx = 0;
90
- if (parts[0] === prefixToStrip) {
91
- startIdx = 1;
92
- }
93
- // Get package name at specified depth
94
- if (parts.length > startIdx) {
95
- // For depth > 1, join multiple segments
96
- const endIdx = Math.min(startIdx + depth, parts.length - 1);
97
- if (endIdx > startIdx) {
98
- return parts.slice(startIdx, endIdx).join('/');
99
- }
100
- return parts[startIdx];
101
- }
102
- return 'core';
103
- }
104
- /**
105
- * Find the matching package for a change using pattern-based strategy.
106
- * Returns the first matching slice's package name, or undefined if no match.
107
- */
108
- function findMatchingPattern(changeName, strategy) {
109
- for (const slice of strategy.slices) {
110
- for (const pattern of slice.patterns) {
111
- if ((0, minimatch_1.minimatch)(changeName, pattern, { dot: true })) {
112
- return slice.packageName;
113
- }
114
- }
115
- }
116
- return undefined;
117
- }
118
- /**
119
- * Assign changes to packages based on grouping strategy
120
- */
121
- function assignChangesToPackages(graph, strategy, defaultPackage = 'core') {
122
- const assignments = new Map();
123
- for (const [changeName] of graph.nodes) {
124
- let packageName;
125
- switch (strategy.type) {
126
- case 'folder': {
127
- const depth = strategy.depth ?? 1;
128
- const prefix = strategy.prefixToStrip ?? 'schemas';
129
- packageName = extractPackageFromPath(changeName, depth, prefix);
130
- break;
131
- }
132
- case 'pattern': {
133
- const matched = findMatchingPattern(changeName, strategy);
134
- packageName = matched || defaultPackage;
135
- break;
136
- }
137
- case 'explicit': {
138
- packageName = strategy.mapping[changeName] || defaultPackage;
139
- break;
140
- }
141
- default:
142
- packageName = defaultPackage;
143
- }
144
- // Fallback to default package
145
- if (!packageName) {
146
- packageName = defaultPackage;
147
- }
148
- if (!assignments.has(packageName)) {
149
- assignments.set(packageName, new Set());
150
- }
151
- assignments.get(packageName).add(changeName);
152
- }
153
- return assignments;
154
- }
155
- /**
156
- * Merge small packages into larger ones
157
- */
158
- function mergeSmallPackages(assignments, minChanges, defaultPackage = 'core') {
159
- const result = new Map();
160
- // Ensure default package exists
161
- if (!result.has(defaultPackage)) {
162
- result.set(defaultPackage, new Set());
163
- }
164
- for (const [pkg, changes] of assignments) {
165
- if (changes.size < minChanges && pkg !== defaultPackage) {
166
- // Merge into default package
167
- const defaultChanges = result.get(defaultPackage);
168
- for (const change of changes) {
169
- defaultChanges.add(change);
170
- }
171
- }
172
- else {
173
- result.set(pkg, new Set(changes));
174
- }
175
- }
176
- // Remove empty default package if it exists
177
- if (result.get(defaultPackage)?.size === 0) {
178
- result.delete(defaultPackage);
179
- }
180
- return result;
181
- }
182
- /**
183
- * Build package-level dependency graph
184
- */
185
- function buildPackageDependencies(graph, assignments) {
186
- const packageDeps = new Map();
187
- // Build reverse lookup: change -> package
188
- const changeToPackage = new Map();
189
- for (const [pkg, changes] of assignments) {
190
- packageDeps.set(pkg, new Set());
191
- for (const change of changes) {
192
- changeToPackage.set(change, pkg);
193
- }
194
- }
195
- // Check each change's dependencies
196
- for (const [changeName, deps] of graph.edges) {
197
- const myPackage = changeToPackage.get(changeName);
198
- if (!myPackage)
199
- continue;
200
- for (const dep of deps) {
201
- const depPackage = changeToPackage.get(dep);
202
- if (depPackage && depPackage !== myPackage) {
203
- // Cross-package dependency
204
- packageDeps.get(myPackage).add(depPackage);
205
- }
206
- }
207
- }
208
- return packageDeps;
209
- }
210
- /**
211
- * Detect cycles in package dependency graph
212
- */
213
- function detectPackageCycle(deps) {
214
- const visited = new Set();
215
- const visiting = new Set();
216
- function dfs(pkg, path) {
217
- if (visiting.has(pkg)) {
218
- return [...path, pkg];
219
- }
220
- if (visited.has(pkg))
221
- return null;
222
- visiting.add(pkg);
223
- const pkgDeps = deps.get(pkg) || new Set();
224
- for (const dep of pkgDeps) {
225
- const cycle = dfs(dep, [...path, pkg]);
226
- if (cycle)
227
- return cycle;
228
- }
229
- visiting.delete(pkg);
230
- visited.add(pkg);
231
- return null;
232
- }
233
- for (const pkg of deps.keys()) {
234
- const cycle = dfs(pkg, []);
235
- if (cycle)
236
- return cycle;
237
- }
238
- return null;
239
- }
240
- /**
241
- * Compute deployment order for packages (topological sort)
242
- */
243
- function computeDeployOrder(packageDeps) {
244
- const order = [];
245
- const visited = new Set();
246
- const allPackages = new Set();
247
- // Collect all packages
248
- for (const pkg of packageDeps.keys()) {
249
- allPackages.add(pkg);
250
- }
251
- for (const deps of packageDeps.values()) {
252
- for (const dep of deps) {
253
- allPackages.add(dep);
254
- }
255
- }
256
- function visit(pkg) {
257
- if (visited.has(pkg))
258
- return;
259
- visited.add(pkg);
260
- const deps = packageDeps.get(pkg) || new Set();
261
- for (const dep of deps) {
262
- visit(dep);
263
- }
264
- order.push(pkg);
265
- }
266
- // Visit all packages (sorted for determinism)
267
- for (const pkg of [...allPackages].sort()) {
268
- visit(pkg);
269
- }
270
- return order;
271
- }
272
- /**
273
- * Topological sort of changes within a package
274
- */
275
- function topologicalSortWithinPackage(changes, graph) {
276
- const result = [];
277
- const visited = new Set();
278
- function visit(change) {
279
- if (visited.has(change))
280
- return;
281
- if (!changes.has(change))
282
- return;
283
- visited.add(change);
284
- const deps = graph.edges.get(change) || new Set();
285
- for (const dep of deps) {
286
- if (changes.has(dep)) {
287
- visit(dep);
288
- }
289
- }
290
- result.push(change);
291
- }
292
- // Visit in alphabetical order for determinism
293
- for (const change of [...changes].sort()) {
294
- visit(change);
295
- }
296
- return result;
297
- }
298
- /**
299
- * Generate plan file content for a package
300
- */
301
- function generatePlanContent(pkgName, entries, tags = []) {
302
- let content = `%syntax-version=1.0.0\n`;
303
- content += `%project=${pkgName}\n`;
304
- content += `%uri=${pkgName}\n\n`;
305
- // Dependency brackets keep their original order (empty order map), matching
306
- // the source plans this content is sliced from.
307
- const originalOrder = new Map();
308
- for (const entry of entries) {
309
- content += (0, writer_1.generateChangeLineContent)(entry, originalOrder) + '\n';
310
- // Add tags associated with this change
311
- const changeTags = tags.filter(t => t.change === entry.name);
312
- for (const tag of changeTags) {
313
- content += (0, writer_1.generateTagLineContent)(tag) + '\n';
314
- }
315
- }
316
- return content;
317
- }
318
- /**
319
- * Generate control file content for a package
320
- */
321
- function generateControlContent(pkgName, deps) {
322
- let content = `# ${pkgName} extension\n`;
323
- content += `comment = '${pkgName} module'\n`;
324
- content += `default_version = '0.0.1'\n`;
325
- content += `relocatable = false\n`;
326
- if (deps.size > 0) {
327
- content += `requires = '${[...deps].sort().join(', ')}'\n`;
328
- }
329
- return content;
330
- }
331
- /**
332
- * Generate a single package output
333
- */
334
- function generateSinglePackage(pkgName, changes, graph, changeToPackage, pkgDeps, crossPackageDeps = 'change') {
335
- // Back-compat: earlier signature took useTagsForCrossPackageDeps: boolean
336
- const crossPackageDepMode = crossPackageDeps === true ? 'tag' : crossPackageDeps === false ? 'change' : crossPackageDeps;
337
- // Sort changes in topological order within package
338
- const sortedChanges = topologicalSortWithinPackage(changes, graph);
339
- // Build plan entries with updated dependencies
340
- const planEntries = [];
341
- for (const changeName of sortedChanges) {
342
- const originalChange = graph.nodes.get(changeName);
343
- const deps = graph.edges.get(changeName) || new Set();
344
- const newDeps = [];
345
- for (const dep of deps) {
346
- const depPkg = changeToPackage.get(dep);
347
- if (!depPkg) {
348
- // External dependency (from installed module) - keep as-is
349
- newDeps.push(dep);
350
- }
351
- else if (depPkg === pkgName) {
352
- // Internal dependency - keep as-is
353
- newDeps.push(dep);
354
- }
355
- else {
356
- // Cross-package dependency
357
- if (crossPackageDepMode === 'control-only') {
358
- // Dropped from the plan line; carried by the control file's
359
- // `requires` (extension install ordering deploys the whole
360
- // dependency package first)
361
- continue;
362
- }
363
- if (crossPackageDepMode === 'tag') {
364
- // Find latest tag in the dependency package
365
- const depPkgChanges = [...graph.nodes.keys()]
366
- .filter(c => changeToPackage.get(c) === depPkg);
367
- const lastChange = depPkgChanges[depPkgChanges.length - 1];
368
- const tags = graph.tags.get(lastChange);
369
- if (tags && tags.length > 0) {
370
- const latestTag = tags[tags.length - 1];
371
- newDeps.push(`${depPkg}:@${latestTag.name}`);
372
- }
373
- else {
374
- newDeps.push(`${depPkg}:${dep}`);
375
- }
376
- }
377
- else {
378
- newDeps.push(`${depPkg}:${dep}`);
379
- }
380
- }
381
- }
382
- planEntries.push({
383
- name: changeName,
384
- dependencies: newDeps,
385
- timestamp: originalChange.timestamp,
386
- planner: originalChange.planner,
387
- email: originalChange.email,
388
- comment: originalChange.comment
389
- });
390
- }
391
- // Collect tags for this package
392
- const packageTags = [];
393
- for (const changeName of sortedChanges) {
394
- const changeTags = graph.tags.get(changeName) || [];
395
- packageTags.push(...changeTags);
396
- }
397
- // Generate plan content
398
- const planContent = generatePlanContent(pkgName, planEntries, packageTags);
399
- // Generate control file content
400
- const controlContent = generateControlContent(pkgName, pkgDeps);
401
- return {
402
- name: pkgName,
403
- planContent,
404
- controlContent,
405
- changes: planEntries,
406
- packageDependencies: [...pkgDeps]
407
- };
408
- }
409
- /**
410
- * Main slicing function
411
- */
412
- function slicePlan(config) {
413
- // Parse the source plan file
414
- const planResult = (0, parser_1.parsePlanFile)(config.sourcePlan);
415
- if (!planResult.data) {
416
- const errorMessages = planResult.errors?.map(e => `Line ${e.line}: ${e.message}`).join('\n') || 'Unknown error';
417
- throw new Error(`Failed to parse plan file: ${errorMessages}`);
418
- }
419
- const plan = planResult.data;
420
- const warnings = [];
421
- // Build dependency graph
422
- const graph = buildDependencyGraph(plan);
423
- // Validate DAG
424
- try {
425
- validateDAG(graph);
426
- }
427
- catch (error) {
428
- throw new Error(`Invalid plan file: ${error.message}`);
429
- }
430
- // Assign changes to packages
431
- let assignments = assignChangesToPackages(graph, config.strategy, config.defaultPackage || 'core');
432
- // Merge small packages if configured
433
- if (config.minChangesPerPackage && config.minChangesPerPackage > 1) {
434
- assignments = mergeSmallPackages(assignments, config.minChangesPerPackage, config.defaultPackage || 'core');
435
- }
436
- // Build package dependencies
437
- const packageDeps = buildPackageDependencies(graph, assignments);
438
- // Check for package cycles
439
- const cycle = detectPackageCycle(packageDeps);
440
- if (cycle) {
441
- warnings.push({
442
- type: 'cycle_detected',
443
- message: `Package cycle detected: ${cycle.join(' -> ')}`,
444
- suggestedAction: 'Consider merging these packages or reorganizing dependencies'
445
- });
446
- // For now, we'll proceed but warn - in production we might want to auto-merge
447
- }
448
- // Build reverse lookup
449
- const changeToPackage = new Map();
450
- for (const [pkg, changes] of assignments) {
451
- for (const change of changes) {
452
- changeToPackage.set(change, pkg);
453
- }
454
- }
455
- // Compute deploy order
456
- const deployOrder = computeDeployOrder(packageDeps);
457
- // Generate packages
458
- const packages = [];
459
- for (const pkgName of deployOrder) {
460
- const changes = assignments.get(pkgName);
461
- if (!changes || changes.size === 0)
462
- continue;
463
- const pkgOutput = generateSinglePackage(pkgName, changes, graph, changeToPackage, packageDeps.get(pkgName) || new Set(), config.crossPackageDepMode ?? (config.useTagsForCrossPackageDeps ? 'tag' : 'change'));
464
- packages.push(pkgOutput);
465
- // Check for heavy cross-package dependencies
466
- let crossDeps = 0;
467
- let totalDeps = 0;
468
- for (const change of changes) {
469
- const deps = graph.edges.get(change) || new Set();
470
- for (const dep of deps) {
471
- totalDeps++;
472
- const depPkg = changeToPackage.get(dep);
473
- if (depPkg && depPkg !== pkgName) {
474
- crossDeps++;
475
- }
476
- }
477
- }
478
- const ratio = totalDeps > 0 ? crossDeps / totalDeps : 0;
479
- if (ratio > 0.5) {
480
- warnings.push({
481
- type: 'heavy_cross_deps',
482
- message: `Package "${pkgName}" has ${Math.round(ratio * 100)}% cross-package dependencies`,
483
- suggestedAction: 'Consider merging with dependent packages or reorganizing'
484
- });
485
- }
486
- }
487
- // Calculate stats
488
- let internalEdges = 0;
489
- let crossPackageEdges = 0;
490
- for (const [change, deps] of graph.edges) {
491
- const myPkg = changeToPackage.get(change);
492
- for (const dep of deps) {
493
- const depPkg = changeToPackage.get(dep);
494
- if (depPkg === myPkg) {
495
- internalEdges++;
496
- }
497
- else if (depPkg) {
498
- crossPackageEdges++;
499
- }
500
- }
501
- }
502
- const totalEdges = internalEdges + crossPackageEdges;
503
- return {
504
- packages,
505
- workspace: {
506
- packages: deployOrder,
507
- deployOrder,
508
- dependencies: Object.fromEntries([...packageDeps.entries()].map(([k, v]) => [k, [...v].sort()]))
509
- },
510
- warnings,
511
- stats: {
512
- totalChanges: graph.nodes.size,
513
- packagesCreated: packages.length,
514
- internalEdges,
515
- crossPackageEdges,
516
- crossPackageRatio: totalEdges > 0 ? crossPackageEdges / totalEdges : 0
517
- }
518
- };
519
- }