@quark-hq/quark-scripts 0.0.1

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.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/dist/app/base-application.js +67 -0
  3. package/dist/app/buildApplication.js +20 -0
  4. package/dist/app/copyStaticApplication.js +34 -0
  5. package/dist/app/devApplication.js +59 -0
  6. package/dist/app/prod-publish-application.js +124 -0
  7. package/dist/app/publish-dev-application.js +104 -0
  8. package/dist/app/publishDevApplication.js +16 -0
  9. package/dist/app/release-application.js +85 -0
  10. package/dist/app/releaseApplication.js +154 -0
  11. package/dist/app/unfreeze-application.js +75 -0
  12. package/dist/app/watchStaticApplication.js +34 -0
  13. package/dist/cli/cli-containers.js +70 -0
  14. package/dist/cli/cliCommand.js +2 -0
  15. package/dist/cli/cliContainers.js +62 -0
  16. package/dist/cli/commands/buildCommands.js +17 -0
  17. package/dist/cli/commands/copyStaticCommands.js +17 -0
  18. package/dist/cli/commands/devCommands.js +17 -0
  19. package/dist/cli/commands/prod-publish-commands.js +18 -0
  20. package/dist/cli/commands/publish-dev-commands.js +17 -0
  21. package/dist/cli/commands/publishDevCommands.js +17 -0
  22. package/dist/cli/commands/release-commands.js +17 -0
  23. package/dist/cli/commands/releaseCommands.js +17 -0
  24. package/dist/cli/commands/relesaeCommand.js +15 -0
  25. package/dist/cli/commands/unfreeze-commands.js +17 -0
  26. package/dist/cli/commands/watchStaticCommands.js +17 -0
  27. package/dist/cli/program.js +16 -0
  28. package/dist/commands/index.d.ts +8 -0
  29. package/dist/commands/index.js +132 -0
  30. package/dist/commands/index.js.map +1 -0
  31. package/dist/domain/graph.js +2 -0
  32. package/dist/domain/new-package-freeze-from-map.js +21 -0
  33. package/dist/domain/resolve-workspace-dependency-specifier-for-freeze-map.js +33 -0
  34. package/dist/domain/reverse-dependents.js +45 -0
  35. package/dist/domain/topological-sorting.js +43 -0
  36. package/dist/domain/transitive-dependents.js +25 -0
  37. package/dist/domain/workspace-transitive-dependencies.js +25 -0
  38. package/dist/errors/unfreeze-blocked-error.js +44 -0
  39. package/dist/index.d.ts +2 -0
  40. package/dist/index.js +32 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/infrastructure/adapters/base-release-adapter.js +281 -0
  43. package/dist/infrastructure/adapters/maven-prod-publish-adapter.js +48 -0
  44. package/dist/infrastructure/adapters/maven-release-adapter.js +58 -0
  45. package/dist/infrastructure/adapters/node-prod-publish-adapter.js +111 -0
  46. package/dist/infrastructure/adapters/node-release-adapter/node-file-update.js +1 -0
  47. package/dist/infrastructure/adapters/node-release-adapter/node-release-adapter.js +289 -0
  48. package/dist/infrastructure/adapters/node-release-adapter.js +202 -0
  49. package/dist/infrastructure/config/loadDotEnv.js +38 -0
  50. package/dist/infrastructure/config/nodeRegistryEnv.js +41 -0
  51. package/dist/infrastructure/config/quarkConfigProvider.js +50 -0
  52. package/dist/infrastructure/git/gitService.js +179 -0
  53. package/dist/infrastructure/graph/nxGraphProvide.js +84 -0
  54. package/dist/infrastructure/logging/consoleLogger.js +25 -0
  55. package/dist/infrastructure/process/nodeProcessRunner.js +14 -0
  56. package/dist/infrastructure/release/git-release-map-store.js +64 -0
  57. package/dist/ports/config.js +2 -0
  58. package/dist/ports/git.js +2 -0
  59. package/dist/ports/graph.js +2 -0
  60. package/dist/ports/logger.js +2 -0
  61. package/dist/ports/map.js +2 -0
  62. package/dist/ports/platform-dev-publish-adapter.js +2 -0
  63. package/dist/ports/platform-prod-publish-adapter.js +2 -0
  64. package/dist/ports/platform-release-adapter.js +2 -0
  65. package/dist/ports/processRunner.js +2 -0
  66. package/dist/ports/prompts.js +2 -0
  67. package/dist/ports/release-map-store.js +2 -0
  68. package/dist/scripts/dev.d.ts +20 -0
  69. package/dist/scripts/dev.js +204 -0
  70. package/dist/scripts/dev.js.map +1 -0
  71. package/dist/scripts/map.json +1616 -0
  72. package/dist/scripts/prod.d.ts +1 -0
  73. package/dist/scripts/prod.js +143 -0
  74. package/dist/scripts/prod.js.map +1 -0
  75. package/dist/scripts/release.d.ts +51 -0
  76. package/dist/scripts/release.js +833 -0
  77. package/dist/scripts/release.js.map +1 -0
  78. package/dist/tests/index.test.js +13 -0
  79. package/dist/tests/mocks/release.js +8 -0
  80. package/dist/tests/unit/release/index.test.js +32 -0
  81. package/dist/utils/checkIfFreeze.js +1 -0
  82. package/dist/utils/commit.js +39 -0
  83. package/dist/utils/drawRepoStatus.js +66 -0
  84. package/dist/utils/file.js +14 -0
  85. package/dist/utils/metrics.js +22 -0
  86. package/package.json +64 -0
@@ -0,0 +1,833 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const child_process_1 = require("child_process");
40
+ const fs_1 = require("fs");
41
+ const path = __importStar(require("path"));
42
+ const util_1 = require("util");
43
+ const prompts_1 = __importDefault(require("prompts"));
44
+ const semver = __importStar(require("semver"));
45
+ const dotenv_1 = __importDefault(require("dotenv"));
46
+ const chalk_1 = __importDefault(require("chalk"));
47
+ const drawRepoStatus_1 = require("../utils/drawRepoStatus");
48
+ const metrics_1 = require("../utils/metrics");
49
+ const commit_1 = require("../utils/commit");
50
+ dotenv_1.default.config({ quiet: true });
51
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
52
+ const MAP_FILE = path.resolve(process.cwd(), '.release/map.json');
53
+ class Release {
54
+ constructor(verbose) {
55
+ this.adjList = {};
56
+ this.invertedAdjList = {};
57
+ this.filesChanged = [];
58
+ this.changedPackages = new Map();
59
+ this.packageJsonCache = new Map();
60
+ this.mainBranchMapJson = null;
61
+ this.packagePathCache = new Map();
62
+ this.nxGraph = null;
63
+ this.repoRoot = null;
64
+ this.frozenPackages = new Set();
65
+ this.excludedPackages = [];
66
+ this.quarkConfig = null;
67
+ this.pendingPackageJsonWrites = new Map();
68
+ if (verbose) {
69
+ console.log(chalk_1.default.gray("🔍 Verbose mode enabled"));
70
+ process.env.VERBOSE = "true";
71
+ }
72
+ this.metrics = {
73
+ startTime: Date.now(),
74
+ phases: {}
75
+ };
76
+ }
77
+ /** Async utility: read JSON file with intelligent caching */
78
+ async readJsonCached(filePath) {
79
+ if (!this.packageJsonCache.has(filePath)) {
80
+ try {
81
+ const content = await fs_1.promises.readFile(filePath, 'utf-8');
82
+ const json = JSON.parse(content);
83
+ this.packageJsonCache.set(filePath, json);
84
+ // Prevent memory bloat - limit cache size
85
+ if (this.packageJsonCache.size > 1000) {
86
+ const firstKey = this.packageJsonCache.keys().next().value;
87
+ if (firstKey) {
88
+ this.packageJsonCache.delete(firstKey);
89
+ }
90
+ }
91
+ }
92
+ catch (error) {
93
+ const errorMessage = error instanceof Error ? error.message : String(error);
94
+ console.warn(`Warning: Failed to read ${filePath}: ${errorMessage}`);
95
+ return null;
96
+ }
97
+ }
98
+ return this.packageJsonCache.get(filePath) || null;
99
+ }
100
+ async readConfig() {
101
+ (0, metrics_1.startPhase)('Reading Config', this.metrics);
102
+ try {
103
+ const config = await fs_1.promises.readFile(path.resolve(process.cwd(), 'quark.config.json'), 'utf-8');
104
+ this.quarkConfig = JSON.parse(config);
105
+ if (this.quarkConfig?.release?.excludePackages) {
106
+ this.excludedPackages = this.quarkConfig.release.excludePackages;
107
+ }
108
+ }
109
+ catch {
110
+ console.log(chalk_1.default.yellow('No quark.config.json found, using default config'));
111
+ this.quarkConfig = {
112
+ release: {
113
+ masterBranch: 'main',
114
+ excludePackages: [],
115
+ autoCommit: false,
116
+ autoBump: true,
117
+ freeze: false
118
+ }
119
+ };
120
+ }
121
+ (0, metrics_1.endPhase)('Reading Config', this.metrics);
122
+ }
123
+ async initializeRepository() {
124
+ (0, metrics_1.startPhase)('Repository Initialization', this.metrics);
125
+ const checks = [];
126
+ try {
127
+ // Run all independent git operations in parallel
128
+ const [repoRootResult, currentBranchResult, fetchResult] = await Promise.allSettled([
129
+ execAsync('git rev-parse --show-toplevel'),
130
+ execAsync('git rev-parse --abbrev-ref HEAD'),
131
+ execAsync(`git fetch origin ${this.quarkConfig?.release?.masterBranch}`)
132
+ ]);
133
+ // Process repository root
134
+ if (repoRootResult.status === 'fulfilled') {
135
+ this.repoRoot = repoRootResult.value.stdout.trim();
136
+ checks.push({
137
+ label: 'Repository Root',
138
+ status: '✓',
139
+ message: this.repoRoot
140
+ });
141
+ }
142
+ else {
143
+ checks.push({
144
+ label: 'Repository Root',
145
+ status: '✗',
146
+ message: 'Failed to get repository root'
147
+ });
148
+ throw new Error('Failed to get repository root');
149
+ }
150
+ // Process current branch
151
+ let currentBranch = '';
152
+ if (currentBranchResult.status === 'fulfilled') {
153
+ currentBranch = currentBranchResult.value.stdout.trim();
154
+ checks.push({
155
+ label: 'Current Branch',
156
+ status: '✓',
157
+ message: currentBranch
158
+ });
159
+ }
160
+ else {
161
+ checks.push({
162
+ label: 'Current Branch',
163
+ status: '✗',
164
+ message: 'Failed to get current branch'
165
+ });
166
+ }
167
+ // Process fetch status
168
+ if (fetchResult.status === 'rejected') {
169
+ checks.push({
170
+ label: `Fetch origin/${this.quarkConfig?.release?.masterBranch}`,
171
+ status: '⚠',
172
+ message: 'Failed to fetch, continuing with local data'
173
+ });
174
+ }
175
+ else {
176
+ checks.push({
177
+ label: `Fetch origin/${this.quarkConfig?.release?.masterBranch}`,
178
+ status: '✓',
179
+ message: 'Successfully fetched latest changes'
180
+ });
181
+ }
182
+ // Check if branch is up-to-date with origin/masterBranch
183
+ if (currentBranch) {
184
+ try {
185
+ const { stdout } = await execAsync(`git rev-list --count HEAD..origin/${this.quarkConfig?.release?.masterBranch}`);
186
+ const missingCount = parseInt(stdout.trim(), 10);
187
+ if (isNaN(missingCount)) {
188
+ checks.push({
189
+ label: 'Branch Sync Status',
190
+ status: '⚠',
191
+ message: 'Could not determine sync status'
192
+ });
193
+ }
194
+ else if (missingCount > 0) {
195
+ checks.push({
196
+ label: 'Branch Sync Status',
197
+ status: '✗',
198
+ message: `${missingCount} commits behind origin/${this.quarkConfig?.release?.masterBranch}`
199
+ });
200
+ (0, drawRepoStatus_1.drawRepoStatusTable)(checks);
201
+ console.error(chalk_1.default.red(`\n❌ Branch is behind origin/${this.quarkConfig?.release?.masterBranch}. Please pull/rebase before continuing.`));
202
+ process.exit(1);
203
+ }
204
+ else {
205
+ checks.push({
206
+ label: 'Branch Sync Status',
207
+ status: '✓',
208
+ message: `Up to date with origin/${this.quarkConfig?.release?.masterBranch}`
209
+ });
210
+ }
211
+ }
212
+ catch (error) {
213
+ checks.push({
214
+ label: 'Branch Sync Status',
215
+ status: '⚠',
216
+ message: `Could not compare with origin/${this.quarkConfig?.release?.masterBranch}`
217
+ });
218
+ }
219
+ }
220
+ // Load map.json from master branch (separate from sync check)
221
+ try {
222
+ const mapContent = await execAsync(`git show origin/${this.quarkConfig?.release?.masterBranch}:.release/map.json`);
223
+ this.mainBranchMapJson = JSON.parse(mapContent.stdout);
224
+ checks.push({
225
+ label: 'Map.json',
226
+ status: '✓',
227
+ message: `Loaded ${Object.keys(this.mainBranchMapJson).length} package entries from ${this.quarkConfig?.release?.masterBranch}`
228
+ });
229
+ }
230
+ catch {
231
+ this.mainBranchMapJson = {};
232
+ checks.push({
233
+ label: 'Map.json',
234
+ status: '⚠',
235
+ message: `No map.json found on origin/${this.quarkConfig?.release?.masterBranch} (first release?)`
236
+ });
237
+ }
238
+ // Draw the status table
239
+ (0, drawRepoStatus_1.drawRepoStatusTable)(checks);
240
+ }
241
+ catch (error) {
242
+ const errorMessage = error instanceof Error ? error.message : String(error);
243
+ // Draw table with error if we have any checks
244
+ if (checks.length > 0) {
245
+ (0, drawRepoStatus_1.drawRepoStatusTable)(checks);
246
+ }
247
+ console.error(chalk_1.default.red('\n❌ Repository initialization failed:'), errorMessage);
248
+ process.exit(1);
249
+ }
250
+ (0, metrics_1.endPhase)('Repository Initialization', this.metrics);
251
+ }
252
+ /** Optimized file change detection with parallel processing */
253
+ async detectChangedFiles() {
254
+ (0, metrics_1.startPhase)('Change Detection', this.metrics);
255
+ try {
256
+ // Run git diff operations in parallel
257
+ const [trackedResult, untrackedResult] = await Promise.allSettled([
258
+ execAsync(`git diff --name-only origin/${this.quarkConfig?.release?.masterBranch}`),
259
+ execAsync('git ls-files --others --exclude-standard')
260
+ ]);
261
+ const trackedFiles = trackedResult.status === 'fulfilled'
262
+ ? trackedResult.value.stdout.split('\n').filter(f => f.trim())
263
+ : [];
264
+ const untrackedFiles = untrackedResult.status === 'fulfilled'
265
+ ? untrackedResult.value.stdout.split('\n').filter(f => f.trim())
266
+ : [];
267
+ // Filter files that are in packages/
268
+ this.filesChanged = [...trackedFiles, ...untrackedFiles];
269
+ console.log(chalk_1.default.blue(`Found ${this.filesChanged.length} changed files`));
270
+ }
271
+ catch (error) {
272
+ const errorMessage = error instanceof Error ? error.message : String(error);
273
+ console.error(chalk_1.default.red('Change detection failed:'), errorMessage);
274
+ this.filesChanged = [];
275
+ }
276
+ (0, metrics_1.endPhase)('Change Detection', this.metrics);
277
+ }
278
+ /** Highly optimized package.json path resolution with caching */
279
+ async getPackageJsonPath(filePath) {
280
+ // Check cache first
281
+ if (this.packagePathCache.has(filePath)) {
282
+ return this.packagePathCache.get(filePath) || null;
283
+ }
284
+ if (!this.repoRoot) {
285
+ return null;
286
+ }
287
+ let currentDir = path.resolve(this.repoRoot, filePath);
288
+ try {
289
+ const stat = await fs_1.promises.stat(currentDir);
290
+ if (stat.isFile()) {
291
+ currentDir = path.dirname(currentDir);
292
+ }
293
+ }
294
+ catch {
295
+ // File might not exist, try directory path
296
+ currentDir = path.dirname(currentDir);
297
+ }
298
+ // Traverse up to find package.json, but stop BEFORE repo root
299
+ // to avoid resolving to the monorepo root package.json
300
+ while (currentDir !== this.repoRoot && currentDir.startsWith(this.repoRoot)) {
301
+ const packageJsonPath = path.join(currentDir, 'package.json');
302
+ try {
303
+ await fs_1.promises.access(packageJsonPath);
304
+ // Cache the result for future lookups
305
+ this.packagePathCache.set(filePath, packageJsonPath);
306
+ return packageJsonPath;
307
+ }
308
+ catch {
309
+ // Continue up the directory tree
310
+ }
311
+ currentDir = path.dirname(currentDir);
312
+ }
313
+ // Cache negative result too
314
+ this.packagePathCache.set(filePath, null);
315
+ return null;
316
+ }
317
+ /** Parallel Nx graph loading and processing */
318
+ async loadAndProcessNxGraph() {
319
+ (0, metrics_1.startPhase)('Nx Graph Processing', this.metrics);
320
+ try {
321
+ // Run Nx graph and force JSON output to stdout
322
+ const { stdout } = await execAsync('npx nx graph --file=stdout', { maxBuffer: 10 * 1024 * 1024 });
323
+ // Parse JSON directly from stdout
324
+ this.nxGraph = JSON.parse(stdout);
325
+ // Build adjacency list
326
+ this.createAdjacencyList();
327
+ }
328
+ catch (error) {
329
+ const errorMessage = error instanceof Error ? error.message : String(error);
330
+ console.error('❌ Nx graph processing failed:', errorMessage);
331
+ throw error;
332
+ }
333
+ (0, metrics_1.endPhase)('Nx Graph Processing', this.metrics);
334
+ }
335
+ /** Build adjacency list and inverted adjacency list in a single pass */
336
+ createAdjacencyList() {
337
+ const adjList = {};
338
+ const invertedAdjList = {};
339
+ const dependencies = this.nxGraph?.graph.dependencies || {};
340
+ Object.keys(dependencies).forEach(source => {
341
+ if (!adjList[source]) {
342
+ adjList[source] = [];
343
+ }
344
+ const deps = dependencies[source] || [];
345
+ deps.forEach(dep => {
346
+ adjList[source].push(dep.target);
347
+ if (!invertedAdjList[dep.target]) {
348
+ invertedAdjList[dep.target] = [];
349
+ }
350
+ invertedAdjList[dep.target].push(source);
351
+ });
352
+ });
353
+ this.adjList = adjList;
354
+ this.invertedAdjList = invertedAdjList;
355
+ }
356
+ /** Optimized package change evaluation with parallel processing */
357
+ async evaluateChangedPackages() {
358
+ (0, metrics_1.startPhase)('Package Evaluation', this.metrics);
359
+ // Process files in parallel batches
360
+ const batchSize = 20;
361
+ const batches = [];
362
+ for (let i = 0; i < this.filesChanged.length; i += batchSize) {
363
+ batches.push(this.filesChanged.slice(i, i + batchSize));
364
+ }
365
+ for (const batch of batches) {
366
+ const promises = batch.map(async (file) => {
367
+ const pkgJsonPath = await this.getPackageJsonPath(file);
368
+ if (!pkgJsonPath)
369
+ return;
370
+ const localPkg = await this.readJsonCached(pkgJsonPath);
371
+ if (!localPkg)
372
+ return;
373
+ this.changedPackages.set(localPkg.name, pkgJsonPath);
374
+ });
375
+ await Promise.allSettled(promises);
376
+ }
377
+ console.log(`📊 Found ${this.changedPackages.size} changed packages`);
378
+ (0, metrics_1.endPhase)('Package Evaluation', this.metrics);
379
+ }
380
+ async checkFrozenPackages(packageName) {
381
+ (0, metrics_1.startPhase)('Checking Frozen Packages', this.metrics);
382
+ // Check if the package is frozen in the mainBranchMapJson
383
+ if (this.mainBranchMapJson &&
384
+ this.mainBranchMapJson[packageName] &&
385
+ this.mainBranchMapJson[packageName].frozen === true) {
386
+ // Load local map.json (bumpMap)
387
+ let localMapJson = {};
388
+ try {
389
+ const bumpMapRaw = await fs_1.promises.readFile(MAP_FILE, 'utf-8');
390
+ localMapJson = JSON.parse(bumpMapRaw);
391
+ }
392
+ catch {
393
+ // If file not found or error, treat as empty
394
+ localMapJson = {};
395
+ }
396
+ // Check if locally set as frozen
397
+ if (localMapJson[packageName] &&
398
+ localMapJson[packageName].frozen === true) {
399
+ // Locally also frozen
400
+ (0, metrics_1.endPhase)('Checking Frozen Packages', this.metrics);
401
+ return true;
402
+ }
403
+ else {
404
+ // Not locally frozen, check pinned dependency versions
405
+ // 1. Find package.json for `packageName`
406
+ let pkgPath = null;
407
+ if (this.nxGraph?.graph?.nodes?.[packageName]?.data?.root) {
408
+ pkgPath = this.nxGraph.graph.nodes[packageName].data.root + '/package.json';
409
+ pkgPath = await this.getPackageJsonPath(pkgPath);
410
+ }
411
+ if (!pkgPath)
412
+ return false;
413
+ const pkg = await this.readJsonCached(pkgPath);
414
+ if (!pkg) {
415
+ (0, metrics_1.endPhase)('Checking Frozen Packages', this.metrics);
416
+ return false;
417
+ }
418
+ // Get all dependencies for this package that are also present in the graph
419
+ const depFields = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
420
+ let atLeastOnePinned = false;
421
+ for (const field of depFields) {
422
+ const deps = pkg[field];
423
+ if (deps && typeof deps === 'object') {
424
+ for (const depName of Object.keys(deps)) {
425
+ // Only interested in dependencies that exist in the nxGraph
426
+ if (!this.nxGraph?.graph?.nodes?.hasOwnProperty(depName))
427
+ continue;
428
+ const version = deps[depName];
429
+ // Check if version is a specific version OR workspace:* OR workspace:^
430
+ if (typeof version === 'string' && (version.startsWith('workspace:*') ||
431
+ version.startsWith('workspace:^') ||
432
+ // matches exact semver pin: i.e. does not contain ^ or ~ or x or *, and does not start with 'workspace:'
433
+ (/^\d+\.\d+\.\d+(-[A-Za-z0-9.+]+)?$/.test(version.trim())))) {
434
+ atLeastOnePinned = true;
435
+ break; // Only need one to satisfy the "else return true"
436
+ }
437
+ }
438
+ if (atLeastOnePinned)
439
+ break;
440
+ }
441
+ }
442
+ if (!atLeastOnePinned) {
443
+ (0, metrics_1.endPhase)('Checking Frozen Packages', this.metrics);
444
+ return false;
445
+ }
446
+ else {
447
+ (0, metrics_1.endPhase)('Checking Frozen Packages', this.metrics);
448
+ return true;
449
+ }
450
+ }
451
+ }
452
+ else {
453
+ // Not frozen in main branch
454
+ (0, metrics_1.endPhase)('Checking Frozen Packages', this.metrics);
455
+ return false;
456
+ }
457
+ }
458
+ /** Optimized version bumping */
459
+ bumpVersion(version, bumpType) {
460
+ return semver.inc(version, bumpType) || version;
461
+ }
462
+ /** DFS for dependent packages using pre-built inverted adjacency list */
463
+ getAllDependentPackages(node) {
464
+ const visited = new Set();
465
+ const dfs = (current) => {
466
+ if (visited.has(current))
467
+ return;
468
+ visited.add(current);
469
+ const dependents = this.invertedAdjList[current] || [];
470
+ for (const dependent of dependents) {
471
+ dfs(dependent);
472
+ }
473
+ };
474
+ dfs(node);
475
+ return visited;
476
+ }
477
+ /** Optimized topological sort */
478
+ topologicalSort() {
479
+ const visited = new Set();
480
+ const temp = new Set();
481
+ const result = [];
482
+ const visit = (node) => {
483
+ if (visited.has(node))
484
+ return;
485
+ if (temp.has(node))
486
+ throw new Error(`Cycle detected involving ${node}`);
487
+ temp.add(node);
488
+ const neighbors = this.adjList[node] || [];
489
+ for (const neighbor of neighbors) {
490
+ visit(neighbor);
491
+ }
492
+ temp.delete(node);
493
+ visited.add(node);
494
+ result.push(node);
495
+ };
496
+ // Sort keys for deterministic behavior
497
+ const sortedKeys = Object.keys(this.adjList);
498
+ for (const node of sortedKeys) {
499
+ if (!visited.has(node)) {
500
+ visit(node);
501
+ }
502
+ }
503
+ return result;
504
+ }
505
+ /** Optimized user interaction with background processing */
506
+ async askForChanges() {
507
+ (0, metrics_1.startPhase)('User Interaction', this.metrics);
508
+ let bumpMap = {};
509
+ try {
510
+ const content = await fs_1.promises.readFile(MAP_FILE, 'utf-8');
511
+ bumpMap = JSON.parse(content);
512
+ }
513
+ catch {
514
+ console.log('📝 Creating new bump map');
515
+ }
516
+ const bumped = new Set();
517
+ const topoSorted = this.topologicalSort();
518
+ const packagesToProcess = topoSorted.filter(pkg => this.changedPackages.has(pkg));
519
+ console.log(`🎯 Processing ${packagesToProcess.length} changed packages...`);
520
+ for (let i = 0; i < packagesToProcess.length; i++) {
521
+ const pkgName = packagesToProcess[i];
522
+ const progress = `[${i + 1}/${packagesToProcess.length}]`;
523
+ if (bumped.has(pkgName) || this.excludedPackages.includes(pkgName) || this.frozenPackages.has(pkgName)) {
524
+ continue;
525
+ }
526
+ const pkgPath = this.changedPackages.get(pkgName);
527
+ const localPkg = await this.readJsonCached(pkgPath);
528
+ const baseVersion = this.mainBranchMapJson?.[pkgName]?.newVersion || "1.0.0";
529
+ const res = await (0, prompts_1.default)([
530
+ {
531
+ type: "select",
532
+ name: "bumpType",
533
+ message: `${progress} 📦 Bump type for ${pkgName} (${baseVersion})?`,
534
+ choices: [
535
+ { title: "New", value: "new" },
536
+ { title: "Patch", value: "patch" },
537
+ { title: "Minor", value: "minor" },
538
+ { title: "Major", value: "major" },
539
+ { title: "Skip", value: null },
540
+ { title: "❌ Cancel", value: "__exit" }
541
+ ]
542
+ },
543
+ {
544
+ type: (prev) => (!prev || prev === "__exit") ? null : "text",
545
+ name: "changeLog",
546
+ message: "📝 Describe the change:"
547
+ }
548
+ ]);
549
+ if (res.bumpType === "__exit") {
550
+ console.log('👋 Cancelled by user');
551
+ process.exit(1);
552
+ }
553
+ if (!res.bumpType) {
554
+ console.log(`${progress} ⏩ Skipped: ${pkgName}`);
555
+ continue;
556
+ }
557
+ let newVersion = this.bumpVersion(baseVersion, res.bumpType);
558
+ if (res.bumpType === "new") {
559
+ if (this.mainBranchMapJson?.[pkgName]) {
560
+ console.log(chalk_1.default.red(`Package ${pkgName} already exists can't create a new package with the same name`));
561
+ process.exit(1);
562
+ }
563
+ if (semver.gt(baseVersion, "1.0.0")) {
564
+ console.log(chalk_1.default.red("Cannot create a new package with a version greater than 1.0.0"));
565
+ process.exit(1);
566
+ }
567
+ newVersion = "1.0.0";
568
+ }
569
+ else {
570
+ if (semver.lt(baseVersion, "1.0.0")) {
571
+ console.log(chalk_1.default.red("I think you are creating a new package please select new instead"));
572
+ process.exit(1);
573
+ }
574
+ if (await this.checkFrozenPackages(pkgName) === true && res.bumpType !== "major") {
575
+ console.log(chalk_1.default.red("Package is frozen can't bump a major version Please unfreeze it using quark unfreeze <packageName>"));
576
+ process.exit(1);
577
+ }
578
+ newVersion = this.bumpVersion(baseVersion, res.bumpType);
579
+ }
580
+ // Accumulate package.json update (deep clone to avoid polluting the cache)
581
+ if (localPkg) {
582
+ const cloned = JSON.parse(JSON.stringify(localPkg));
583
+ cloned.version = newVersion;
584
+ this.pendingPackageJsonWrites.set(pkgPath, JSON.stringify(cloned, null, 2));
585
+ }
586
+ bumpMap[pkgName] = {
587
+ bumpType: res.bumpType,
588
+ baseVersion,
589
+ newVersion,
590
+ changeLog: res.changeLog,
591
+ frozen: false
592
+ };
593
+ bumped.add(pkgName);
594
+ console.log(`${progress} ✅ Updated ${pkgName}: ${baseVersion} → ${newVersion}`);
595
+ // Handle major version cascading
596
+ if (res.bumpType === "major") {
597
+ await this.handleMajorVersionBump(pkgName, bumpMap, bumped);
598
+ }
599
+ }
600
+ // Flush all accumulated package.json writes atomically alongside map.json
601
+ await this.flushPendingPackageJsonWrites();
602
+ await fs_1.promises.writeFile(MAP_FILE, JSON.stringify(bumpMap, null, 2));
603
+ (0, metrics_1.endPhase)('User Interaction', this.metrics);
604
+ }
605
+ /** Write all accumulated package.json changes to disk at once */
606
+ async flushPendingPackageJsonWrites() {
607
+ if (this.pendingPackageJsonWrites.size === 0)
608
+ return;
609
+ console.log(`\n💾 Writing ${this.pendingPackageJsonWrites.size} package.json file(s)...`);
610
+ const writePromises = Array.from(this.pendingPackageJsonWrites.entries()).map(async ([filePath, content]) => {
611
+ await fs_1.promises.writeFile(filePath, content);
612
+ });
613
+ await Promise.all(writePromises);
614
+ this.pendingPackageJsonWrites.clear();
615
+ console.log(`✅ All package.json files updated successfully.`);
616
+ }
617
+ async handleMajorVersionBump(pkgName, bumpMap, bumped) {
618
+ console.log(`🔄 Processing major version bump for ${pkgName}...`);
619
+ let choise = "none";
620
+ if (this.quarkConfig?.release?.freeze) {
621
+ const response = await (0, prompts_1.default)({
622
+ type: "select",
623
+ name: "dependencies",
624
+ message: "Select the dependencies to freeze",
625
+ choices: [
626
+ { title: "All", value: "all" },
627
+ { title: "None", value: "none" },
628
+ { title: "Selective", value: "selective" }
629
+ ]
630
+ });
631
+ choise = response.dependencies;
632
+ }
633
+ const dependents = this.getAllDependentPackages(pkgName);
634
+ dependents.delete(pkgName);
635
+ switch (choise) {
636
+ case "all":
637
+ await this.handleFrozenDependents(pkgName, bumpMap, bumped, dependents);
638
+ break;
639
+ case "none":
640
+ await this.handleMajorVersionCascade(pkgName, bumpMap, bumped, dependents);
641
+ break;
642
+ case "selective":
643
+ await this.handleSelectiveMajorVersionBump(pkgName, bumpMap, bumped, dependents);
644
+ break;
645
+ }
646
+ }
647
+ async handleSelectiveMajorVersionBump(pkgName, bumpMap, bumped, dependents) {
648
+ console.log(`🔄 Processing selective major version bump for ${pkgName}...`);
649
+ let previouslyFrozenDependents = new Set();
650
+ dependents.forEach(async (dep) => {
651
+ const isFrozen = await this.checkFrozenPackages(dep);
652
+ if (isFrozen) {
653
+ previouslyFrozenDependents.add(dep);
654
+ dependents.delete(dep);
655
+ }
656
+ });
657
+ if (previouslyFrozenDependents.size > 0) {
658
+ console.log(chalk_1.default.yellow(`Previously frozen dependents: ${Array.from(previouslyFrozenDependents).join(', ')}`));
659
+ console.log(chalk_1.default.yellow(`If you want to create a bump of any of the above please unfreeze them using quark unfreeze <packageName>`));
660
+ const continueResponse = await (0, prompts_1.default)({
661
+ type: "confirm",
662
+ name: "continue",
663
+ message: "Do you want to continue?",
664
+ initial: true
665
+ });
666
+ if (!continueResponse) {
667
+ console.log('👋 Cancelled by user');
668
+ process.exit(1);
669
+ }
670
+ }
671
+ const response = await (0, prompts_1.default)({
672
+ type: "multiselect",
673
+ name: "dependencies",
674
+ message: "Select the dependencies to freeze",
675
+ choices: Array.from(dependents).map(dep => ({ title: dep, value: dep }))
676
+ });
677
+ const frozenDependents = new Set();
678
+ const unfrozenDependents = new Set();
679
+ dependents.forEach((value) => {
680
+ if (response.dependencies.includes(value)) {
681
+ frozenDependents.add(value);
682
+ }
683
+ else {
684
+ unfrozenDependents.add(value);
685
+ }
686
+ });
687
+ await this.handleMajorVersionCascade(pkgName, bumpMap, bumped, unfrozenDependents);
688
+ await this.handleFrozenDependents(pkgName, bumpMap, bumped, frozenDependents);
689
+ }
690
+ async handleFrozenDependents(pkgName, bumpMap, bumped, dependents) {
691
+ for (const dep of dependents) {
692
+ try {
693
+ if (this.excludedPackages.includes(dep))
694
+ continue;
695
+ // Get package.json path for the dependent
696
+ const depPath = await this.getPackageJsonPath(this.nxGraph?.graph?.nodes?.[dep]?.data?.root + '/package.json');
697
+ if (!depPath)
698
+ continue;
699
+ // Read local version of package.json
700
+ const depLocalPkg = await this.readJsonCached(depPath);
701
+ if (!depLocalPkg || !this.mainBranchMapJson?.[dep]?.newVersion)
702
+ continue;
703
+ // Deep clone to avoid polluting the cache
704
+ const clonedPkg = JSON.parse(JSON.stringify(depLocalPkg));
705
+ // For all dependency fields that are objects in package.json, set all versions to what is in main branch
706
+ const depFields = ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'];
707
+ for (const field of depFields) {
708
+ if (clonedPkg[field] && typeof clonedPkg[field] === 'object') {
709
+ for (const depKey of Object.keys(clonedPkg[field])) {
710
+ if (this.mainBranchMapJson?.[depKey]?.newVersion) {
711
+ clonedPkg[field][depKey] = this.mainBranchMapJson[depKey].newVersion;
712
+ }
713
+ }
714
+ }
715
+ }
716
+ const depBaseVersion = this.mainBranchMapJson[dep].newVersion;
717
+ clonedPkg.version = depBaseVersion;
718
+ // Accumulate the frozen package.json write (deferred until all processing completes)
719
+ this.pendingPackageJsonWrites.set(depPath, JSON.stringify(clonedPkg, null, 2));
720
+ // Mark in bumpMap that this dependent is frozen
721
+ bumpMap[dep] = {
722
+ ...(bumpMap[dep] || {}),
723
+ baseVersion: this.mainBranchMapJson?.[dep]?.newVersion || "1.0.0",
724
+ newVersion: this.mainBranchMapJson?.[dep]?.newVersion || "1.0.0",
725
+ bumpType: this.mainBranchMapJson?.[dep]?.bumpType || "new",
726
+ changeLog: `Dependency version frozen due to major version bump of ${pkgName}`,
727
+ frozen: true
728
+ };
729
+ bumped.add(dep);
730
+ this.frozenPackages.add(dep);
731
+ console.log(` ├─ ❄️ Frozen ${dep} at version ${this.mainBranchMapJson?.[dep]?.newVersion || "1.0.0"}`);
732
+ }
733
+ catch (error) {
734
+ const errorMessage = error instanceof Error ? error.message : String(error);
735
+ console.warn(` ├─ ⚠️ Warning: Failed to freeze ${dep}: ${errorMessage}`);
736
+ continue;
737
+ }
738
+ }
739
+ }
740
+ /** Sequential major version cascade handling to avoid race conditions */
741
+ async handleMajorVersionCascade(pkgName, bumpMap, bumped, dependents) {
742
+ console.log(`🔄 Processing major version cascade for ${pkgName}...`);
743
+ let successful = 0;
744
+ for (const dep of dependents) {
745
+ if (bumped.has(dep))
746
+ continue;
747
+ try {
748
+ if (this.excludedPackages.includes(dep))
749
+ continue;
750
+ const depPath = await this.getPackageJsonPath(this.nxGraph?.graph?.nodes?.[dep]?.data?.root + '/package.json');
751
+ if (!depPath)
752
+ continue;
753
+ const depLocalPkg = await this.readJsonCached(depPath);
754
+ if (!depLocalPkg)
755
+ continue;
756
+ const depBaseVersion = this.mainBranchMapJson?.[dep]?.newVersion || "1.0.0";
757
+ const depNewVersion = this.bumpVersion(depBaseVersion, "major");
758
+ // Deep clone to avoid polluting the cache
759
+ const clonedPkg = JSON.parse(JSON.stringify(depLocalPkg));
760
+ clonedPkg.version = depNewVersion;
761
+ // Accumulate the write (deferred until all processing completes)
762
+ this.pendingPackageJsonWrites.set(depPath, JSON.stringify(clonedPkg, null, 2));
763
+ bumpMap[dep] = {
764
+ bumpType: "major",
765
+ baseVersion: depBaseVersion,
766
+ newVersion: depNewVersion,
767
+ changeLog: `Auto-bumped due to ${pkgName} major release`,
768
+ frozen: false
769
+ };
770
+ bumped.add(dep);
771
+ successful++;
772
+ console.log(` ├─ 📈 Auto-bumped ${dep}: ${depBaseVersion} → ${depNewVersion}`);
773
+ }
774
+ catch (error) {
775
+ const errorMessage = error instanceof Error ? error.message : String(error);
776
+ console.warn(` ├─ ⚠️ Warning: Failed to cascade to ${dep}: ${errorMessage}`);
777
+ }
778
+ }
779
+ console.log(`🔄 Cascaded major version to ${successful} dependent packages`);
780
+ }
781
+ /** Performance summary */
782
+ printPerformanceSummary() {
783
+ const totalTime = Date.now() - this.metrics.startTime;
784
+ console.log('\n📊 Performance Summary:');
785
+ console.log(`⏱️ Total time: ${totalTime}ms`);
786
+ Object.entries(this.metrics.phases).forEach(([phase, data]) => {
787
+ if (data.duration) {
788
+ const percentage = ((data.duration / totalTime) * 100).toFixed(1);
789
+ console.log(` ├─ ${phase}: ${data.duration}ms (${percentage}%)`);
790
+ }
791
+ });
792
+ const memUsage = process.memoryUsage();
793
+ console.log(`💾 Peak memory: ${(memUsage.heapUsed / 1024 / 1024).toFixed(2)}MB`);
794
+ }
795
+ async commitChanges() {
796
+ const bumpMap = await fs_1.promises.readFile(MAP_FILE, 'utf-8');
797
+ const commitMessage = (0, commit_1.generateMarkdownCommitMessageFromMapJson)(JSON.parse(bumpMap));
798
+ console.log(commitMessage);
799
+ await execAsync('git add .');
800
+ await execAsync(`git commit -m "${commitMessage}"`);
801
+ console.log('🔄 Changes committed');
802
+ }
803
+ /** Main execution method */
804
+ // This is a Promise because the run() method performs asynchronous operations using 'await'.
805
+ async run() {
806
+ try {
807
+ await this.readConfig();
808
+ await this.initializeRepository();
809
+ await this.detectChangedFiles();
810
+ if (this.filesChanged.length === 0) {
811
+ console.log(chalk_1.default.yellow('No changes detected in packages/'));
812
+ return;
813
+ }
814
+ await this.loadAndProcessNxGraph();
815
+ await this.evaluateChangedPackages();
816
+ await this.askForChanges();
817
+ if (this.quarkConfig?.release?.autoCommit) {
818
+ await this.commitChanges();
819
+ }
820
+ console.log('\n🎉 Release process completed successfully!');
821
+ this.printPerformanceSummary();
822
+ }
823
+ catch (error) {
824
+ const errorMessage = error instanceof Error ? error.message : String(error);
825
+ console.error('\n❌ Release process failed:', errorMessage);
826
+ if (process.env.DEBUG && error instanceof Error) {
827
+ console.error(error.stack);
828
+ }
829
+ process.exit(1);
830
+ }
831
+ }
832
+ }
833
+ exports.default = Release;