@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.
- package/README.md +36 -22
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/pr-preview.d.ts +3 -0
- package/dist/cli/pr-preview.d.ts.map +1 -0
- package/dist/cli/validate-workflows.d.ts +3 -0
- package/dist/cli/validate-workflows.d.ts.map +1 -0
- package/dist/graph/analysis.d.ts +6 -0
- package/dist/graph/analysis.d.ts.map +1 -0
- package/dist/graph/builder.d.ts +3 -0
- package/dist/graph/builder.d.ts.map +1 -0
- package/dist/graph/traversal.d.ts +5 -0
- package/dist/graph/traversal.d.ts.map +1 -0
- package/dist/graph/types.d.ts +47 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/index-cli.js +1094 -0
- package/dist/index-cli.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +738 -0
- package/dist/index.js.map +1 -0
- package/dist/types/clients.d.ts +15 -0
- package/dist/types/clients.d.ts.map +1 -0
- package/dist/workflow/discovery.d.ts +4 -0
- package/dist/workflow/discovery.d.ts.map +1 -0
- package/dist/workflow/expected-paths.d.ts +13 -0
- package/dist/workflow/expected-paths.d.ts.map +1 -0
- package/dist/workflow/parser.d.ts +3 -0
- package/dist/workflow/parser.d.ts.map +1 -0
- package/dist/workflow/policy.d.ts +3 -0
- package/dist/workflow/policy.d.ts.map +1 -0
- package/dist/workflow/types.d.ts +34 -0
- package/dist/workflow/types.d.ts.map +1 -0
- package/dist/workflow/validator.d.ts +3 -0
- package/dist/workflow/validator.d.ts.map +1 -0
- package/dist/workspace/discovery.d.ts +12 -0
- package/dist/workspace/discovery.d.ts.map +1 -0
- package/dist/workspace/file-mapping.d.ts +4 -0
- package/dist/workspace/file-mapping.d.ts.map +1 -0
- package/dist/workspace/package-map.d.ts +12 -0
- package/dist/workspace/package-map.d.ts.map +1 -0
- package/package.json +49 -45
- package/src/cli/pr-preview.ts +0 -388
- package/src/cli/validate-workflows.ts +0 -847
- package/src/graph/analysis.ts +0 -147
- package/src/graph/builder.ts +0 -52
- package/src/graph/traversal.ts +0 -132
- package/src/graph/types.ts +0 -50
- package/src/index.test.ts +0 -94
- package/src/index.ts +0 -40
- package/src/types/clients.ts +0 -19
- package/src/workspace/discovery.ts +0 -94
- package/src/workspace/file-mapping.ts +0 -35
package/src/cli/pr-preview.ts
DELETED
|
@@ -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();
|