@crossplatformai/dependency-graph 0.9.3 → 0.10.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.
Files changed (53) hide show
  1. package/README.md +36 -22
  2. package/dist/cli/index.d.ts +3 -0
  3. package/dist/cli/index.d.ts.map +1 -0
  4. package/dist/cli/pr-preview.d.ts +3 -0
  5. package/dist/cli/pr-preview.d.ts.map +1 -0
  6. package/dist/cli/validate-workflows.d.ts +3 -0
  7. package/dist/cli/validate-workflows.d.ts.map +1 -0
  8. package/dist/graph/analysis.d.ts +6 -0
  9. package/dist/graph/analysis.d.ts.map +1 -0
  10. package/dist/graph/builder.d.ts +3 -0
  11. package/dist/graph/builder.d.ts.map +1 -0
  12. package/dist/graph/traversal.d.ts +5 -0
  13. package/dist/graph/traversal.d.ts.map +1 -0
  14. package/dist/graph/types.d.ts +47 -0
  15. package/dist/graph/types.d.ts.map +1 -0
  16. package/dist/index-cli.js +1094 -0
  17. package/dist/index-cli.js.map +1 -0
  18. package/dist/index.d.ts +13 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +738 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/types/clients.d.ts +15 -0
  23. package/dist/types/clients.d.ts.map +1 -0
  24. package/dist/workflow/discovery.d.ts +4 -0
  25. package/dist/workflow/discovery.d.ts.map +1 -0
  26. package/dist/workflow/expected-paths.d.ts +13 -0
  27. package/dist/workflow/expected-paths.d.ts.map +1 -0
  28. package/dist/workflow/parser.d.ts +3 -0
  29. package/dist/workflow/parser.d.ts.map +1 -0
  30. package/dist/workflow/policy.d.ts +3 -0
  31. package/dist/workflow/policy.d.ts.map +1 -0
  32. package/dist/workflow/types.d.ts +34 -0
  33. package/dist/workflow/types.d.ts.map +1 -0
  34. package/dist/workflow/validator.d.ts +3 -0
  35. package/dist/workflow/validator.d.ts.map +1 -0
  36. package/dist/workspace/discovery.d.ts +12 -0
  37. package/dist/workspace/discovery.d.ts.map +1 -0
  38. package/dist/workspace/file-mapping.d.ts +4 -0
  39. package/dist/workspace/file-mapping.d.ts.map +1 -0
  40. package/dist/workspace/package-map.d.ts +12 -0
  41. package/dist/workspace/package-map.d.ts.map +1 -0
  42. package/package.json +49 -45
  43. package/src/cli/pr-preview.ts +0 -388
  44. package/src/cli/validate-workflows.ts +0 -847
  45. package/src/graph/analysis.ts +0 -147
  46. package/src/graph/builder.ts +0 -52
  47. package/src/graph/traversal.ts +0 -132
  48. package/src/graph/types.ts +0 -50
  49. package/src/index.test.ts +0 -94
  50. package/src/index.ts +0 -40
  51. package/src/types/clients.ts +0 -19
  52. package/src/workspace/discovery.ts +0 -94
  53. package/src/workspace/file-mapping.ts +0 -35
@@ -1,388 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { readFileSync } from 'node:fs';
4
- import { resolve } from 'node:path';
5
- import { glob } from 'glob';
6
- import { parse as parseYaml } from 'yaml';
7
-
8
- // Import dependency graph functions from parent module
9
- import {
10
- discoverWorkspaces,
11
- buildDependencyGraph,
12
- findAffectedPackages,
13
- type WorkspacePackage,
14
- } from '../index.js';
15
-
16
- interface ChangedFile {
17
- path: string;
18
- status: 'modified' | 'added' | 'deleted';
19
- }
20
-
21
- interface DeploymentIndicator {
22
- file?: string;
23
- field?: string;
24
- dependency?: string;
25
- }
26
-
27
- interface PlatformDetection {
28
- platform: string;
29
- indicators: DeploymentIndicator[];
30
- }
31
-
32
- const WORKFLOW_MAPPING: Record<string, string> = {
33
- 'deploy-web.yml': 'web',
34
- 'deploy-api-origin.yml': 'api-origin',
35
- 'deploy-api-edge.yml': 'api-edge',
36
- 'release-mobile.yml': 'mobile',
37
- 'release-desktop.yml': 'desktop',
38
- 'release-cli.yml': 'cli',
39
- };
40
-
41
- // Convention-based deployment detection (order matters - most specific first)
42
- const PLATFORM_INDICATORS: PlatformDetection[] = [
43
- {
44
- platform: 'cloudflare-workers',
45
- indicators: [{ file: 'wrangler.toml' }],
46
- },
47
- {
48
- platform: 'expo',
49
- indicators: [{ file: 'app.json' }, { file: 'eas.json' }],
50
- },
51
- {
52
- platform: 'npm-package',
53
- indicators: [{ field: 'bin' }],
54
- },
55
- {
56
- platform: 'electron',
57
- indicators: [
58
- { dependency: 'electron' },
59
- { dependency: 'electron-builder' },
60
- ],
61
- },
62
- {
63
- platform: 'next.js',
64
- indicators: [
65
- { file: 'next.config.js' },
66
- { file: 'next.config.mjs' },
67
- { file: 'next.config.ts' },
68
- { dependency: 'next' },
69
- ],
70
- },
71
- {
72
- platform: 'node.js',
73
- indicators: [
74
- { field: 'start' }, // has start script
75
- ],
76
- },
77
- ];
78
-
79
- const DEFAULT_APP_DIRECTORIES = ['apps'];
80
-
81
- function isDeployableApp(pkg: WorkspacePackage): {
82
- deployable: boolean;
83
- platform?: string;
84
- } {
85
- // Check if package is in apps directory (configurable in future)
86
- const isInAppDirectory = DEFAULT_APP_DIRECTORIES.some(
87
- (dir) => pkg.path.includes(`/${dir}/`) || pkg.path.endsWith(`/${dir}`),
88
- );
89
-
90
- if (!isInAppDirectory) {
91
- return { deployable: false };
92
- }
93
-
94
- // Detect platform based on indicators
95
- for (const platformDetection of PLATFORM_INDICATORS) {
96
- const hasIndicators = platformDetection.indicators.some((indicator) => {
97
- if (indicator.file) {
98
- try {
99
- const filePath = resolve(pkg.path, indicator.file);
100
- readFileSync(filePath, 'utf-8');
101
- return true;
102
- } catch {
103
- return false;
104
- }
105
- }
106
-
107
- if (indicator.field) {
108
- const scripts = pkg.packageJson.scripts as
109
- | Record<string, string>
110
- | undefined;
111
- if (indicator.field === 'start' && scripts?.start) {
112
- return true;
113
- }
114
- const packageJsonField =
115
- pkg.packageJson[indicator.field as keyof typeof pkg.packageJson];
116
- if (indicator.field !== 'start' && packageJsonField) {
117
- return true;
118
- }
119
- }
120
-
121
- if (indicator.dependency) {
122
- return !!(
123
- pkg.dependencies[indicator.dependency] ||
124
- pkg.devDependencies[indicator.dependency]
125
- );
126
- }
127
-
128
- return false;
129
- });
130
-
131
- if (hasIndicators) {
132
- return { deployable: true, platform: platformDetection.platform };
133
- }
134
- }
135
-
136
- // If in apps directory but no specific platform detected, not deployable
137
- return { deployable: false };
138
- }
139
-
140
- async function getChangedFiles(): Promise<ChangedFile[]> {
141
- const { execSync } = await import('node:child_process');
142
-
143
- try {
144
- // Try local production first, then fall back to origin/production
145
- let baseBranch = 'production';
146
- try {
147
- execSync('git rev-parse production', {
148
- encoding: 'utf-8',
149
- stdio: 'pipe',
150
- });
151
- } catch {
152
- baseBranch = 'origin/production';
153
- }
154
-
155
- const output = execSync(`git diff --name-status ${baseBranch}...HEAD`, {
156
- encoding: 'utf-8',
157
- });
158
-
159
- return output
160
- .trim()
161
- .split('\n')
162
- .filter((line) => line)
163
- .map((line) => {
164
- const [status, path] = line.split('\t');
165
-
166
- return {
167
- path: path ?? '',
168
- status:
169
- status === 'D' ? 'deleted' : status === 'A' ? 'added' : 'modified',
170
- };
171
- })
172
- .filter((file): file is ChangedFile => file.path !== '');
173
- } catch {
174
- console.error(
175
- 'Failed to get changed files. Ensure you are on a branch with commits compared to production.',
176
- );
177
- process.exit(1);
178
- }
179
- }
180
-
181
- async function main() {
182
- try {
183
- const rootDir = resolve(process.cwd());
184
- const changedFiles = await getChangedFiles();
185
-
186
- console.log(
187
- `\nšŸ“¦ Release Preview - Changed Files: ${changedFiles.length}\n`,
188
- );
189
-
190
- changedFiles.forEach((file) => {
191
- console.log(` ${file.status === 'deleted' ? 'āŒ' : 'šŸ“'} ${file.path}`);
192
- });
193
-
194
- const packages = await discoverWorkspaces(rootDir, {
195
- fs: {
196
- readFile: (path) => {
197
- try {
198
- return Promise.resolve(readFileSync(path, 'utf-8'));
199
- } catch {
200
- return Promise.reject(new Error(`Failed to read file: ${path}`));
201
- }
202
- },
203
- exists: (path) => {
204
- try {
205
- readFileSync(path, 'utf-8');
206
- return Promise.resolve(true);
207
- } catch {
208
- return Promise.resolve(false);
209
- }
210
- },
211
- },
212
- glob: {
213
- glob: async (pattern, options) => glob(pattern, options),
214
- },
215
- yaml: {
216
- parse: (content) => parseYaml(content) as Record<string, unknown>,
217
- },
218
- });
219
-
220
- const graph = buildDependencyGraph(packages);
221
- const changedPackages = new Set<string>();
222
-
223
- for (const file of changedFiles) {
224
- if (file.status === 'deleted') continue;
225
-
226
- // Convert absolute package paths to relative for comparison
227
- const pkg = packages.find((p) => {
228
- const relativePkgPath = p.path.replace(rootDir + '/', '');
229
- return file.path.startsWith(relativePkgPath);
230
- });
231
-
232
- if (pkg) {
233
- changedPackages.add(pkg.name);
234
- }
235
- }
236
-
237
- const changedWorkflows = changedFiles.filter((f) =>
238
- f.path.startsWith('.github/workflows/'),
239
- );
240
-
241
- const workflowAffectedApps = new Set<string>();
242
- for (const workflow of changedWorkflows) {
243
- const workflowName = workflow.path.split('/').pop();
244
- if (workflowName && WORKFLOW_MAPPING[workflowName]) {
245
- workflowAffectedApps.add(WORKFLOW_MAPPING[workflowName]);
246
- }
247
- }
248
-
249
- console.log(
250
- `\nšŸ“¦ Changed packages: ${Array.from(changedPackages).join(', ') || 'none'}\n`,
251
- );
252
-
253
- if (changedPackages.size === 0) {
254
- console.log('\n✨ No packages changed - no deployments needed\n');
255
- return;
256
- }
257
-
258
- const affected = findAffectedPackages(changedPackages, graph, {
259
- direction: 'upstream',
260
- respectAffectsUpstream: true,
261
- });
262
-
263
- // Find all deployable apps (affected and unaffected)
264
- const allDeployableApps = packages
265
- .map((pkg) => {
266
- const detection = isDeployableApp(pkg);
267
- return detection.deployable
268
- ? { name: pkg.name, pkg, platform: detection.platform }
269
- : null;
270
- })
271
- .filter(
272
- (
273
- item,
274
- ): item is {
275
- name: string;
276
- pkg: WorkspacePackage;
277
- platform: string | undefined;
278
- } => item !== null,
279
- );
280
-
281
- // Categorize apps by deployment type
282
- const affectedApps = allDeployableApps.filter(
283
- (app): app is NonNullable<typeof app> =>
284
- affected.has(app.name) || workflowAffectedApps.has(app.name),
285
- );
286
- const unaffectedApps = allDeployableApps.filter(
287
- (app): app is NonNullable<typeof app> =>
288
- !affected.has(app.name) && !workflowAffectedApps.has(app.name),
289
- );
290
-
291
- // Separate deploys (web-based) vs releases (installed)
292
- const getAppCategory = (platform?: string) => {
293
- switch (platform) {
294
- case 'next.js':
295
- case 'cloudflare-workers':
296
- case 'node.js':
297
- return 'deploy';
298
- case 'expo':
299
- case 'electron':
300
- case 'npm-package':
301
- return 'release';
302
- default:
303
- return 'deploy'; // default to deploy for generic
304
- }
305
- };
306
-
307
- const getAppIcon = (platform?: string) => {
308
- switch (platform) {
309
- case 'next.js':
310
- case 'cloudflare-workers':
311
- case 'node.js':
312
- return '🌐';
313
- case 'expo':
314
- return 'šŸ“±';
315
- case 'electron':
316
- return 'šŸ–„ļø';
317
- case 'npm-package':
318
- return '⚔';
319
- default:
320
- return '🌐';
321
- }
322
- };
323
-
324
- const affectedDeploys = affectedApps.filter(
325
- (app): app is NonNullable<typeof app> =>
326
- getAppCategory(app.platform) === 'deploy',
327
- );
328
- const affectedReleases = affectedApps.filter(
329
- (app): app is NonNullable<typeof app> =>
330
- getAppCategory(app.platform) === 'release',
331
- );
332
-
333
- console.log(`\nšŸš€ PR Preview\n`);
334
-
335
- if (affectedApps.length === 0) {
336
- console.log('No apps affected by changes.\n');
337
- } else {
338
- if (affectedDeploys.length > 0) {
339
- console.log('šŸ“‹ Apps that will be DEPLOYED:');
340
- for (const item of affectedDeploys) {
341
- const platformDisplay =
342
- item.platform === 'generic' ? '' : ` (${item.platform})`;
343
- const icon = getAppIcon(item.platform);
344
- console.log(`${icon} ${item.name}${platformDisplay}`);
345
- }
346
- console.log('');
347
- }
348
-
349
- if (affectedReleases.length > 0) {
350
- console.log('šŸ“‹ Apps that will be RELEASED:');
351
- for (const item of affectedReleases) {
352
- const platformDisplay =
353
- item.platform === 'generic' ? '' : ` (${item.platform})`;
354
- const icon = getAppIcon(item.platform);
355
- console.log(`${icon} ${item.name}${platformDisplay}`);
356
- }
357
- console.log('');
358
- }
359
- }
360
-
361
- if (unaffectedApps.length > 0) {
362
- console.log(`šŸ“‹ Apps that will NOT be affected:`);
363
- for (const item of unaffectedApps) {
364
- const platformDisplay =
365
- item.platform === 'generic' ? '' : ` (${item.platform})`;
366
- console.log(`ā­ļø ${item.name}${platformDisplay}`);
367
- }
368
- console.log('');
369
- }
370
-
371
- console.log(`Legend:`);
372
- console.log(`🌐 = Deploy (web-based, instant updates)`);
373
- console.log(`šŸ“± = Release (mobile app, user installs)`);
374
- console.log(`šŸ–„ļø = Release (desktop app, user installs)`);
375
- console.log(`⚔ = Release (CLI tool, user installs)`);
376
- console.log(`ā­ļø = Unaffected (no changes needed)`);
377
- console.log(`šŸ“ = File changed`);
378
- console.log(`āŒ = File deleted\n`);
379
- } catch (error) {
380
- console.error(
381
- 'Error:',
382
- error instanceof Error ? error.message : String(error),
383
- );
384
- process.exit(1);
385
- }
386
- }
387
-
388
- void main();