parche-cli 0.1.1 → 0.5.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.
@@ -0,0 +1,107 @@
1
+ // `parche scan --upgrades` y `parche diff`: para cada SDK de npm que el repo usa, compara la versión
2
+ // instalada con la última publicada y dice qué operaciones del repo cambian o desaparecen.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { extractSurface } from './surface.js';
6
+ import { diffSurfaces, affectedOperations, unverifiedOperations, dedupeChanges } from './compare.js';
7
+ import { compareVersions } from './registry.js';
8
+
9
+ const readJson = (p) => {
10
+ try {
11
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
12
+ } catch {
13
+ return null;
14
+ }
15
+ };
16
+
17
+ // Versión instalada: node_modules → package-lock.json → la base del rango declarado ("^3.2.1" → 3.2.1).
18
+ // Si la base no existe en el registry, la más alta de la misma versión mayor. → { version, from } | null
19
+ export function installedVersion(root, pkg, declared, meta) {
20
+ const nm = readJson(path.join(root, 'node_modules', pkg, 'package.json'));
21
+ if (nm?.version) return { version: nm.version, from: 'node_modules' };
22
+ const lock = readJson(path.join(root, 'package-lock.json'));
23
+ const locked = lock?.packages?.[`node_modules/${pkg}`]?.version || lock?.dependencies?.[pkg]?.version;
24
+ if (locked) return { version: locked, from: 'package-lock.json' };
25
+ const m = String(declared || '').match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
26
+ if (!m) return null;
27
+ const base = `${m[1]}.${m[2] || 0}.${m[3] || 0}`;
28
+ if (!meta || meta.versions[base]) return { version: base, from: 'package.json' };
29
+ const sameMajor = Object.keys(meta.versions).filter((v) => v.split('.')[0] === m[1] && !v.includes('-')).sort(compareVersions);
30
+ return sameMajor.length ? { version: sameMajor[sameMajor.length - 1], from: 'package.json' } : null;
31
+ }
32
+
33
+ // "latest", "4" o "4.1" → la versión exacta más alta que corresponde, según el registry.
34
+ export function resolveSpec(meta, spec) {
35
+ if (spec === 'latest') return meta['dist-tags']?.latest || null;
36
+ if (meta.versions[spec]) return spec;
37
+ const matches = Object.keys(meta.versions).filter((v) => !v.includes('-') && v.startsWith(`${spec}.`)).sort(compareVersions);
38
+ return matches.length ? matches[matches.length - 1] : null;
39
+ }
40
+
41
+ // Diff completo entre dos versiones de un paquete (acepta "latest" y versiones parciales).
42
+ export async function diffVersions(registry, pkg, fromSpec, toSpec) {
43
+ const meta = await registry.metadata(pkg);
44
+ const from = resolveSpec(meta, fromSpec);
45
+ const to = resolveSpec(meta, toSpec);
46
+ if (!from || !to) return { pkg, from: fromSpec, to: toSpec, error: `${pkg}: no existe la versión ${!from ? fromSpec : toSpec} en el registry.` };
47
+ const [ta, tb] = await Promise.all([registry.types(pkg, from), registry.types(pkg, to)]);
48
+ if (!ta || !tb) return { pkg, from, to, error: `No hay tipos (.d.ts) para ${!ta ? from : to}: no se puede comparar.` };
49
+ const sa = extractSurface({ files: ta.files, entry: ta.entry, pkgName: pkg });
50
+ const sb = extractSurface({ files: tb.files, entry: tb.entry, pkgName: pkg });
51
+ const all = diffSurfaces(sa, sb);
52
+ // changes: para mostrar (sin repetidos); allChanges: para cruzar con lo que usa el repo.
53
+ return { pkg, from, to, typesFrom: ta.source, typesTo: tb.source, before: sa, changes: dedupeChanges(all), allChanges: all };
54
+ }
55
+
56
+ // Para cada SDK de npm del inventario que tiene operaciones: versión instalada vs última.
57
+ export async function checkUpgrades(inventory, root, registry) {
58
+ const results = [];
59
+ const sdks = inventory.apis.filter((a) => a.kind === 'sdk' && a.ecosystem === 'npm' && a.operations.length);
60
+ for (const api of sdks) {
61
+ const base = { package: api.package, provider: api.provider };
62
+ try {
63
+ const meta = await registry.metadata(api.package);
64
+ const latest = meta['dist-tags']?.latest;
65
+ const installed = installedVersion(root, api.package, api.version, meta);
66
+ if (!latest || !installed) {
67
+ results.push({ ...base, status: 'sin-version', installed: installed?.version || null, latest: latest || null });
68
+ continue;
69
+ }
70
+ if (compareVersions(installed.version, latest) >= 0) {
71
+ results.push({ ...base, status: 'al-dia', installed: installed.version, latest });
72
+ continue;
73
+ }
74
+ const d = await diffVersions(registry, api.package, installed.version, latest);
75
+ if (d.error) {
76
+ results.push({ ...base, status: 'sin-tipos', installed: installed.version, latest, error: d.error });
77
+ continue;
78
+ }
79
+ const ops = api.operations.map((o) => o.op);
80
+ const affected = affectedOperations(d.allChanges, ops, d.before).map((a) => ({
81
+ ...a, locations: api.operations.find((o) => o.op === a.op).locations,
82
+ }));
83
+ const unverified = unverifiedOperations(ops, d.before);
84
+ const status = affected.length ? 'afectado' : unverified.length === ops.length ? 'sin-verificar' : 'sin-impacto';
85
+ results.push({ ...base, status, installed: installed.version, installedFrom: installed.from, latest,
86
+ totalChanges: d.changes.length, affected, unverified });
87
+ } catch (err) {
88
+ const notPublished = /respondió 404/.test(err.message);
89
+ results.push({ ...base, status: notPublished ? 'no-publicado' : 'error',
90
+ error: notPublished ? `${api.package} no está publicado en el registry de npm (¿paquete privado o interno?).` : err.message });
91
+ }
92
+ }
93
+ return results;
94
+ }
95
+
96
+ // Hallazgos detect-only (en castellano) para el reporte.
97
+ export function upgradeFindings(results) {
98
+ const findings = [];
99
+ for (const r of results.filter((x) => x.status === 'afectado')) {
100
+ for (const a of r.affected) {
101
+ const where = a.locations[0];
102
+ findings.push({ pack: 'sdk.upgrades', file: where.file, line: where.line, status: 'detect-only',
103
+ message: `${r.package} ${r.installed} → ${r.latest}: ${a.changes.map((c) => c.message).join(' ')}` });
104
+ }
105
+ }
106
+ return findings;
107
+ }
package/src/verify.js CHANGED
@@ -3,6 +3,55 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { spawnSync } from 'node:child_process';
5
5
  import { parseCheck } from './ast.js';
6
+ import { isPhp } from './php.js';
7
+
8
+ const readJson = (root, name) => {
9
+ try {
10
+ return JSON.parse(fs.readFileSync(path.join(root, name), 'utf8'));
11
+ } catch {
12
+ return null;
13
+ }
14
+ };
15
+
16
+ // Tests que corresponden a los archivos modificados: `npm test` para JS/TS; para PHP,
17
+ // `composer test` o, si no hay script, vendor/bin/phpunit. Si no hay ninguno, queda el chequeo de sintaxis.
18
+ export function testCommands(root, changedFiles) {
19
+ const cmds = [];
20
+ const pkg = readJson(root, 'package.json');
21
+ if (changedFiles.some((f) => !isPhp(f)) && pkg?.scripts?.test) {
22
+ cmds.push({ label: 'npm test', cmd: 'npm', args: ['test', '--silent'] });
23
+ }
24
+ if (changedFiles.some(isPhp)) {
25
+ const composer = readJson(root, 'composer.json');
26
+ if (composer?.scripts?.test) cmds.push({ label: 'composer test', cmd: 'composer', args: ['test'] });
27
+ else if (fs.existsSync(path.join(root, 'vendor', 'bin', 'phpunit'))) {
28
+ cmds.push({ label: 'vendor/bin/phpunit', cmd: path.join('vendor', 'bin', 'phpunit'), args: [] });
29
+ }
30
+ }
31
+ return cmds;
32
+ }
33
+
34
+ // Workflows del repo que corren en cada pull request: si Parche no corrió los tests, el PR dice dónde se corren.
35
+ // `on: [push, pull_request]`, `on:` + `pull_request:` o `- pull_request`. No cuenta pull_request_target:
36
+ // corre el código de la rama base, no el del PR.
37
+ const ON_PULL_REQUEST = /^\s*(['"]?on['"]?\s*:.*\bpull_request\b|-?\s*pull_request\b\s*(:|#|$))/m;
38
+
39
+ export function prWorkflows(root) {
40
+ const dir = path.join(root, '.github', 'workflows');
41
+ let names = [];
42
+ try {
43
+ names = fs.readdirSync(dir).filter((f) => /\.ya?ml$/i.test(f)).sort();
44
+ } catch {
45
+ return [];
46
+ }
47
+ return names.filter((f) => {
48
+ try {
49
+ return ON_PULL_REQUEST.test(fs.readFileSync(path.join(dir, f), 'utf8'));
50
+ } catch {
51
+ return false;
52
+ }
53
+ }).map((f) => `.github/workflows/${f}`);
54
+ }
6
55
 
7
56
  export function verify(root, changedFiles, { runTests = false } = {}) {
8
57
  const errors = [];
@@ -15,16 +64,23 @@ export function verify(root, changedFiles, { runTests = false } = {}) {
15
64
  }
16
65
  const result = { parse: { ok: errors.length === 0, errors } };
17
66
  if (runTests) {
18
- const pkgPath = path.join(root, 'package.json');
19
- const pkg = fs.existsSync(pkgPath) ? JSON.parse(fs.readFileSync(pkgPath, 'utf8')) : {};
20
- if (!pkg.scripts || !pkg.scripts.test) {
21
- result.tests = { ok: true, skipped: true };
67
+ const cmds = testCommands(root, changedFiles);
68
+ if (!cmds.length) {
69
+ result.tests = { ok: true, skipped: true, commands: [] };
22
70
  } else {
23
- // En Windows npm es npm.cmd y Node solo lo ejecuta a través de un shell.
24
- const r = spawnSync('npm', ['test', '--silent'], { cwd: root, encoding: 'utf8', timeout: 300000, shell: process.platform === 'win32' });
25
- const output = (r.stdout || '') + (r.stderr || '') + (r.error ? `\n${r.error.message}` : '');
26
- result.tests = { ok: r.status === 0, output: output.slice(-2000) };
71
+ let ok = true;
72
+ let output = '';
73
+ for (const c of cmds) {
74
+ // En Windows npm, composer y phpunit son .cmd/.bat y Node solo los ejecuta a través de un shell.
75
+ const r = spawnSync(c.cmd, c.args, { cwd: root, encoding: 'utf8', timeout: 300000, shell: process.platform === 'win32' });
76
+ output += `$ ${c.label}\n${r.stdout || ''}${r.stderr || ''}${r.error ? `\n${r.error.message}` : ''}\n`;
77
+ if (r.status !== 0) { ok = false; break; }
78
+ }
79
+ result.tests = { ok, commands: cmds.map((c) => c.label), output: output.slice(-2000) };
27
80
  }
81
+ } else {
82
+ // Sin --verify, Parche no ejecuta código del repo (por ejemplo, en un repo de terceros): se dice, no se oculta.
83
+ result.tests = { ok: true, notRun: true, ci: prWorkflows(root) };
28
84
  }
29
85
  result.ok = result.parse.ok && (!result.tests || result.tests.ok);
30
86
  return result;