@grafema/util 0.3.18 → 0.3.21
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/dist/core/FileOverview.d.ts.map +1 -1
- package/dist/core/FileOverview.js +16 -12
- package/dist/core/FileOverview.js.map +1 -1
- package/dist/federation/FederatedRouter.d.ts +124 -0
- package/dist/federation/FederatedRouter.d.ts.map +1 -0
- package/dist/federation/FederatedRouter.js +297 -0
- package/dist/federation/FederatedRouter.js.map +1 -0
- package/dist/federation/ShardDiscovery.d.ts +56 -0
- package/dist/federation/ShardDiscovery.d.ts.map +1 -0
- package/dist/federation/ShardDiscovery.js +100 -0
- package/dist/federation/ShardDiscovery.js.map +1 -0
- package/dist/federation/index.d.ts +28 -0
- package/dist/federation/index.d.ts.map +1 -0
- package/dist/federation/index.js +26 -0
- package/dist/federation/index.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/manifest/generator.d.ts.map +1 -1
- package/dist/manifest/generator.js +38 -5
- package/dist/manifest/generator.js.map +1 -1
- package/dist/manifest/index.d.ts +2 -0
- package/dist/manifest/index.d.ts.map +1 -1
- package/dist/manifest/index.js +1 -0
- package/dist/manifest/index.js.map +1 -1
- package/dist/manifest/registry.d.ts +116 -0
- package/dist/manifest/registry.d.ts.map +1 -0
- package/dist/manifest/registry.js +638 -0
- package/dist/manifest/registry.js.map +1 -0
- package/dist/manifest/resolver.d.ts +9 -0
- package/dist/manifest/resolver.d.ts.map +1 -1
- package/dist/manifest/resolver.js +31 -0
- package/dist/manifest/resolver.js.map +1 -1
- package/dist/notation/traceRenderer.d.ts +2 -0
- package/dist/notation/traceRenderer.d.ts.map +1 -1
- package/dist/notation/traceRenderer.js +6 -5
- package/dist/notation/traceRenderer.js.map +1 -1
- package/package.json +3 -3
- package/src/core/FileOverview.ts +16 -11
- package/src/federation/FederatedRouter.ts +440 -0
- package/src/federation/ShardDiscovery.ts +130 -0
- package/src/federation/index.ts +35 -0
- package/src/index.ts +16 -1
- package/src/manifest/generator.ts +37 -5
- package/src/manifest/index.ts +2 -0
- package/src/manifest/registry.ts +769 -0
- package/src/manifest/resolver.ts +33 -0
- package/src/notation/traceRenderer.ts +8 -5
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RegistryBuilder — builds a local manifest registry from npm packages.
|
|
3
|
+
*
|
|
4
|
+
* Resolves installed npm packages, analyzes each via grafema-orchestrator,
|
|
5
|
+
* generates manifest.yaml with ManifestGenerator, and writes to a registry/
|
|
6
|
+
* directory with an index.yaml catalog.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const builder = new RegistryBuilder({ projectPath: '.', registryDir: './registry' });
|
|
10
|
+
* await builder.buildPackage('commander');
|
|
11
|
+
* await builder.buildAll();
|
|
12
|
+
* await builder.writeIndex();
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync, rmSync, cpSync } from 'fs';
|
|
15
|
+
import { join, resolve, dirname } from 'path';
|
|
16
|
+
import { createRequire } from 'module';
|
|
17
|
+
import { spawn } from 'child_process';
|
|
18
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
19
|
+
import { tmpdir } from 'os';
|
|
20
|
+
import { GRAFEMA_VERSION } from '../version.js';
|
|
21
|
+
import { findOrchestratorBinary } from '../utils/findRfdbBinary.js';
|
|
22
|
+
import { ensureBinary } from '../utils/lazyDownload.js';
|
|
23
|
+
import { RFDBServerBackend } from '../storage/backends/RFDBServerBackend.js';
|
|
24
|
+
import { ManifestGenerator } from './generator.js';
|
|
25
|
+
// ── Package Resolution ─────────────────────────────────────
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a package's directory from the project root.
|
|
28
|
+
*
|
|
29
|
+
* Strategy:
|
|
30
|
+
* 1. Try createRequire from project root (works for direct deps)
|
|
31
|
+
* 2. Try createRequire from each workspace package (pnpm strict mode)
|
|
32
|
+
* 3. Walk pnpm store directly (fallback for deeply nested deps)
|
|
33
|
+
*/
|
|
34
|
+
export function resolvePackageDir(packageName, projectPath) {
|
|
35
|
+
// Collect resolution roots: project root + workspace packages
|
|
36
|
+
const roots = [projectPath];
|
|
37
|
+
// Discover workspace packages for pnpm monorepo resolution
|
|
38
|
+
const pnpmWorkspace = join(projectPath, 'pnpm-workspace.yaml');
|
|
39
|
+
if (existsSync(pnpmWorkspace)) {
|
|
40
|
+
// Quick scan: find package.json files in packages/*/
|
|
41
|
+
const packagesDir = join(projectPath, 'packages');
|
|
42
|
+
if (existsSync(packagesDir)) {
|
|
43
|
+
try {
|
|
44
|
+
const entries = readdirSync(packagesDir, { withFileTypes: true });
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (entry.isDirectory()) {
|
|
47
|
+
const pkgPath = join(packagesDir, entry.name);
|
|
48
|
+
if (existsSync(join(pkgPath, 'package.json'))) {
|
|
49
|
+
roots.push(pkgPath);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Ignore readdir errors
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Try createRequire from each root
|
|
60
|
+
for (const root of roots) {
|
|
61
|
+
const resolved = resolvePackageFromRoot(packageName, root);
|
|
62
|
+
if (resolved)
|
|
63
|
+
return resolved;
|
|
64
|
+
}
|
|
65
|
+
// Fallback: walk pnpm store directly
|
|
66
|
+
return resolveFromPnpmStore(packageName, projectPath);
|
|
67
|
+
}
|
|
68
|
+
function resolvePackageFromRoot(packageName, root) {
|
|
69
|
+
const req = createRequire(join(root, 'package.json'));
|
|
70
|
+
// Strategy 1: resolve <pkg>/package.json directly
|
|
71
|
+
try {
|
|
72
|
+
const pkgJsonPath = req.resolve(`${packageName}/package.json`);
|
|
73
|
+
const dir = dirname(pkgJsonPath);
|
|
74
|
+
// Verify this is the actual package root (not dist/cjs/package.json etc.)
|
|
75
|
+
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
76
|
+
if (pkg.name === packageName)
|
|
77
|
+
return dir;
|
|
78
|
+
// Wrong package.json (e.g., dist/cjs/package.json with just {"type":"commonjs"})
|
|
79
|
+
// — walk up from this location to find the real root
|
|
80
|
+
return walkUpToPackageRoot(packageName, dir);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// package.json subpath not exported or not resolvable
|
|
84
|
+
}
|
|
85
|
+
// Strategy 2: resolve main entry, walk up to find package root
|
|
86
|
+
try {
|
|
87
|
+
const mainPath = req.resolve(packageName);
|
|
88
|
+
return walkUpToPackageRoot(packageName, dirname(mainPath));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Not resolvable from this root
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
function walkUpToPackageRoot(packageName, startDir) {
|
|
96
|
+
let dir = startDir;
|
|
97
|
+
for (let i = 0; i < 10; i++) {
|
|
98
|
+
if (existsSync(join(dir, 'package.json'))) {
|
|
99
|
+
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'));
|
|
100
|
+
if (pkg.name === packageName)
|
|
101
|
+
return dir;
|
|
102
|
+
}
|
|
103
|
+
const parent = dirname(dir);
|
|
104
|
+
if (parent === dir)
|
|
105
|
+
break;
|
|
106
|
+
dir = parent;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
function resolveFromPnpmStore(packageName, projectPath) {
|
|
111
|
+
const nodeModules = join(projectPath, 'node_modules');
|
|
112
|
+
const pnpmDir = join(nodeModules, '.pnpm');
|
|
113
|
+
if (!existsSync(pnpmDir))
|
|
114
|
+
return null;
|
|
115
|
+
try {
|
|
116
|
+
// pnpm store structure: .pnpm/<name>@<version>/node_modules/<name>/
|
|
117
|
+
// For scoped: .pnpm/@scope+name@<version>/node_modules/@scope/name/
|
|
118
|
+
const escapedName = packageName.replace('/', '+');
|
|
119
|
+
const entries = readdirSync(pnpmDir);
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
if (entry.startsWith(`${escapedName}@`)) {
|
|
122
|
+
const candidate = join(pnpmDir, entry, 'node_modules', packageName);
|
|
123
|
+
if (existsSync(join(candidate, 'package.json'))) {
|
|
124
|
+
return candidate;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Ignore store access errors
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Detect source type of an npm package.
|
|
136
|
+
*
|
|
137
|
+
* - `dts_only`: Only .d.ts files (e.g., @types/node)
|
|
138
|
+
* - `compiled_js`: Standard npm package with JS output
|
|
139
|
+
* - `minified`: Bundled output with runtime helpers (esbuild, webpack, rollup)
|
|
140
|
+
* - `source`: Has src/ with .ts files (rare for npm packages)
|
|
141
|
+
*/
|
|
142
|
+
export function detectSourceType(packageDir) {
|
|
143
|
+
const pkgJsonPath = join(packageDir, 'package.json');
|
|
144
|
+
if (!existsSync(pkgJsonPath))
|
|
145
|
+
return 'compiled_js';
|
|
146
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
147
|
+
// @types/* packages are .d.ts only
|
|
148
|
+
if (pkgJson.name?.startsWith('@types/'))
|
|
149
|
+
return 'dts_only';
|
|
150
|
+
// Check for types-only (e.g., has "types" but no "main"/"exports")
|
|
151
|
+
if (pkgJson.types && !pkgJson.main && !pkgJson.exports)
|
|
152
|
+
return 'dts_only';
|
|
153
|
+
// Check for source TypeScript
|
|
154
|
+
if (existsSync(join(packageDir, 'src', 'index.ts')))
|
|
155
|
+
return 'source';
|
|
156
|
+
// Detect bundler output (esbuild, webpack, rollup) by checking entry file
|
|
157
|
+
const entryPoint = resolveEntryPoint(pkgJson);
|
|
158
|
+
if (entryPoint) {
|
|
159
|
+
const entryPath = join(packageDir, entryPoint.replace(/^\.\//, ''));
|
|
160
|
+
if (existsSync(entryPath)) {
|
|
161
|
+
try {
|
|
162
|
+
// Read first 500 bytes — bundler signatures are in the header
|
|
163
|
+
const fd = readFileSync(entryPath, { encoding: 'utf-8', flag: 'r' });
|
|
164
|
+
const header = fd.slice(0, 500);
|
|
165
|
+
// esbuild: var __defProp = Object.defineProperty; var __export = ...
|
|
166
|
+
// webpack: /******/ (() => { // webpackBootstrap
|
|
167
|
+
// rollup: usually clean, not detectable this way
|
|
168
|
+
if (header.includes('var __defProp = Object.defineProperty') ||
|
|
169
|
+
header.includes('__webpack_require__') ||
|
|
170
|
+
header.includes('webpackBootstrap')) {
|
|
171
|
+
return 'minified';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Ignore read errors
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return 'compiled_js';
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Resolve entry point from package.json fields.
|
|
183
|
+
* Follows Node.js resolution order: exports["."] → main → index.js
|
|
184
|
+
*/
|
|
185
|
+
export function resolveEntryPoint(pkgJson) {
|
|
186
|
+
// 1. exports["."]
|
|
187
|
+
const exports = pkgJson.exports;
|
|
188
|
+
if (exports) {
|
|
189
|
+
const dot = exports['.'];
|
|
190
|
+
if (typeof dot === 'string')
|
|
191
|
+
return dot;
|
|
192
|
+
if (dot && typeof dot === 'object') {
|
|
193
|
+
const dotObj = dot;
|
|
194
|
+
// Prefer: import → require → default
|
|
195
|
+
const entry = dotObj.import ?? dotObj.require ?? dotObj.default;
|
|
196
|
+
if (typeof entry === 'string')
|
|
197
|
+
return entry;
|
|
198
|
+
// Nested conditions (e.g., { import: { types: ..., default: ... } })
|
|
199
|
+
if (entry && typeof entry === 'object') {
|
|
200
|
+
const nested = entry;
|
|
201
|
+
if (nested.default)
|
|
202
|
+
return nested.default;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// 2. main field
|
|
207
|
+
if (typeof pkgJson.main === 'string')
|
|
208
|
+
return pkgJson.main;
|
|
209
|
+
// 3. module field (ESM)
|
|
210
|
+
if (typeof pkgJson.module === 'string')
|
|
211
|
+
return pkgJson.module;
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
// ── RegistryBuilder ────────────────────────────────────────
|
|
215
|
+
export class RegistryBuilder {
|
|
216
|
+
options;
|
|
217
|
+
results = [];
|
|
218
|
+
info;
|
|
219
|
+
debug;
|
|
220
|
+
constructor(options) {
|
|
221
|
+
this.options = {
|
|
222
|
+
projectPath: resolve(options.projectPath),
|
|
223
|
+
registryDir: resolve(options.registryDir),
|
|
224
|
+
effectsDbPath: options.effectsDbPath,
|
|
225
|
+
maxFiles: options.maxFiles ?? 5000,
|
|
226
|
+
timeout: options.timeout ?? 600_000,
|
|
227
|
+
force: options.force ?? false,
|
|
228
|
+
verbose: options.verbose ?? false,
|
|
229
|
+
skip: options.skip ?? [],
|
|
230
|
+
};
|
|
231
|
+
this.info = console.log;
|
|
232
|
+
this.debug = this.options.verbose ? console.log : () => { };
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Discover all external (non-workspace) dependencies from package.json
|
|
236
|
+
* files in the project, including workspace packages.
|
|
237
|
+
*/
|
|
238
|
+
discoverDependencies() {
|
|
239
|
+
const deps = new Set();
|
|
240
|
+
const collectDeps = (pkgPath) => {
|
|
241
|
+
if (!existsSync(pkgPath))
|
|
242
|
+
return;
|
|
243
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
244
|
+
for (const key of ['dependencies', 'devDependencies']) {
|
|
245
|
+
const section = pkg[key];
|
|
246
|
+
if (!section)
|
|
247
|
+
continue;
|
|
248
|
+
for (const [name, version] of Object.entries(section)) {
|
|
249
|
+
if (version.startsWith('workspace:'))
|
|
250
|
+
continue;
|
|
251
|
+
deps.add(name);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
// Root package.json
|
|
256
|
+
collectDeps(join(this.options.projectPath, 'package.json'));
|
|
257
|
+
// Workspace packages (pnpm monorepo)
|
|
258
|
+
const packagesDir = join(this.options.projectPath, 'packages');
|
|
259
|
+
if (existsSync(packagesDir)) {
|
|
260
|
+
try {
|
|
261
|
+
const entries = readdirSync(packagesDir, { withFileTypes: true });
|
|
262
|
+
for (const entry of entries) {
|
|
263
|
+
if (entry.isDirectory()) {
|
|
264
|
+
collectDeps(join(packagesDir, entry.name, 'package.json'));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// Ignore readdir errors
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return [...deps].sort();
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Build manifest for a single package.
|
|
276
|
+
*/
|
|
277
|
+
async buildPackage(packageName) {
|
|
278
|
+
const startTime = Date.now();
|
|
279
|
+
if (this.options.skip.includes(packageName)) {
|
|
280
|
+
return this.recordResult(packageName, '', false, 'Skipped (in skip list)', startTime);
|
|
281
|
+
}
|
|
282
|
+
// 1. Resolve package directory
|
|
283
|
+
const packageDir = resolvePackageDir(packageName, this.options.projectPath);
|
|
284
|
+
if (!packageDir) {
|
|
285
|
+
return this.recordResult(packageName, '', false, 'Package not found in node_modules', startTime);
|
|
286
|
+
}
|
|
287
|
+
// 2. Read package.json
|
|
288
|
+
const pkgJsonPath = join(packageDir, 'package.json');
|
|
289
|
+
if (!existsSync(pkgJsonPath)) {
|
|
290
|
+
return this.recordResult(packageName, '', false, 'No package.json', startTime);
|
|
291
|
+
}
|
|
292
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
293
|
+
const version = pkgJson.version ?? '0.0.0';
|
|
294
|
+
// 3. Check if already built (unless --force)
|
|
295
|
+
const manifestDir = this.getManifestDir(packageName, version);
|
|
296
|
+
const manifestPath = join(manifestDir, 'manifest.yaml');
|
|
297
|
+
if (!this.options.force && existsSync(manifestPath)) {
|
|
298
|
+
this.debug(` [skip] ${packageName}@${version} — already built`);
|
|
299
|
+
return this.recordResult(packageName, version, true, undefined, startTime, manifestPath);
|
|
300
|
+
}
|
|
301
|
+
// 4. Detect source type
|
|
302
|
+
const sourceType = detectSourceType(packageDir);
|
|
303
|
+
if (sourceType === 'dts_only') {
|
|
304
|
+
this.debug(` [skip] ${packageName}@${version} — dts_only (no runtime code)`);
|
|
305
|
+
return this.recordResult(packageName, version, false, 'dts_only packages not analyzable', startTime);
|
|
306
|
+
}
|
|
307
|
+
if (sourceType === 'minified') {
|
|
308
|
+
// Emit a stub manifest — package exists but exports aren't statically resolvable
|
|
309
|
+
this.debug(` [stub] ${packageName}@${version} — bundled output, emitting stub manifest`);
|
|
310
|
+
const manifestDir = this.getManifestDir(packageName, version);
|
|
311
|
+
const manifestPath = join(manifestDir, 'manifest.yaml');
|
|
312
|
+
mkdirSync(manifestDir, { recursive: true });
|
|
313
|
+
const stub = {
|
|
314
|
+
schema_version: 1,
|
|
315
|
+
analyzer_version: GRAFEMA_VERSION,
|
|
316
|
+
authored: false,
|
|
317
|
+
confidence: 0,
|
|
318
|
+
generated: new Date().toISOString(),
|
|
319
|
+
package: { purl: `pkg:npm/${packageName}@${version}`, source_type: 'minified' },
|
|
320
|
+
exports: [],
|
|
321
|
+
imports: [],
|
|
322
|
+
capabilities: { total_exports: 0, total_internal_symbols: 0, has_graph: false },
|
|
323
|
+
};
|
|
324
|
+
writeFileSync(manifestPath, stringifyYaml(stub), 'utf-8');
|
|
325
|
+
this.info(` [stub] ${packageName}@${version} — bundled/minified, exports not statically resolvable`);
|
|
326
|
+
return this.recordResult(packageName, version, true, undefined, startTime, manifestPath);
|
|
327
|
+
}
|
|
328
|
+
// 5. Resolve entry point
|
|
329
|
+
const entryPoint = resolveEntryPoint(pkgJson);
|
|
330
|
+
this.debug(` Entry: ${entryPoint ?? '(auto-detect)'}`);
|
|
331
|
+
this.debug(` Source: ${sourceType}`);
|
|
332
|
+
this.debug(` Dir: ${packageDir}`);
|
|
333
|
+
// 6. Analyze via orchestrator + generate manifest
|
|
334
|
+
try {
|
|
335
|
+
const manifest = await this.analyzeAndGenerate(packageName, version, packageDir, sourceType, entryPoint);
|
|
336
|
+
// 7. Write manifest to registry
|
|
337
|
+
mkdirSync(manifestDir, { recursive: true });
|
|
338
|
+
const yaml = ManifestGenerator.toYaml(manifest);
|
|
339
|
+
writeFileSync(manifestPath, yaml, 'utf-8');
|
|
340
|
+
this.info(` [ok] ${packageName}@${version} — ${manifest.exports.length} exports, confidence ${manifest.confidence.toFixed(2)}`);
|
|
341
|
+
return this.recordResult(packageName, version, true, undefined, startTime, manifestPath);
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
345
|
+
this.info(` [fail] ${packageName}@${version} — ${message}`);
|
|
346
|
+
return this.recordResult(packageName, version, false, message, startTime);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Build manifests for all discovered dependencies.
|
|
351
|
+
*/
|
|
352
|
+
async buildAll() {
|
|
353
|
+
const deps = this.discoverDependencies();
|
|
354
|
+
this.info(`\nDiscovered ${deps.length} external dependencies\n`);
|
|
355
|
+
for (const dep of deps) {
|
|
356
|
+
this.info(`Building ${dep}...`);
|
|
357
|
+
await this.buildPackage(dep);
|
|
358
|
+
}
|
|
359
|
+
return this.results;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Build manifests for specific packages.
|
|
363
|
+
*/
|
|
364
|
+
async buildPackages(names) {
|
|
365
|
+
for (const name of names) {
|
|
366
|
+
this.info(`Building ${name}...`);
|
|
367
|
+
await this.buildPackage(name);
|
|
368
|
+
}
|
|
369
|
+
return this.results;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Write index.yaml catalog from all manifests in the registry.
|
|
373
|
+
* Merges with existing index to preserve entries from previous runs.
|
|
374
|
+
*/
|
|
375
|
+
writeIndex() {
|
|
376
|
+
const registryDir = this.options.registryDir;
|
|
377
|
+
if (!existsSync(registryDir)) {
|
|
378
|
+
mkdirSync(registryDir, { recursive: true });
|
|
379
|
+
}
|
|
380
|
+
// Load existing index entries (preserve from previous runs)
|
|
381
|
+
const indexPath = join(registryDir, 'index.yaml');
|
|
382
|
+
const existing = new Map();
|
|
383
|
+
if (existsSync(indexPath)) {
|
|
384
|
+
try {
|
|
385
|
+
const prev = parseYaml(readFileSync(indexPath, 'utf-8'));
|
|
386
|
+
if (prev?.entries) {
|
|
387
|
+
for (const entry of prev.entries) {
|
|
388
|
+
existing.set(`${entry.name}@${entry.version}`, entry);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
// Ignore malformed index
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
// Add/update entries from current results
|
|
397
|
+
for (const result of this.results) {
|
|
398
|
+
if (!result.success || !result.manifestPath)
|
|
399
|
+
continue;
|
|
400
|
+
if (!existsSync(result.manifestPath))
|
|
401
|
+
continue;
|
|
402
|
+
try {
|
|
403
|
+
const raw = readFileSync(result.manifestPath, 'utf-8');
|
|
404
|
+
const manifest = parseYaml(raw);
|
|
405
|
+
existing.set(`${result.name}@${result.version}`, {
|
|
406
|
+
name: result.name,
|
|
407
|
+
version: result.version,
|
|
408
|
+
purl: manifest.package.purl,
|
|
409
|
+
source_type: manifest.package.source_type,
|
|
410
|
+
confidence: manifest.confidence,
|
|
411
|
+
total_exports: manifest.exports.length,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
catch {
|
|
415
|
+
// Skip malformed manifests
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const index = {
|
|
419
|
+
schema_version: 1,
|
|
420
|
+
generated: new Date().toISOString(),
|
|
421
|
+
analyzer_version: GRAFEMA_VERSION,
|
|
422
|
+
entries: [...existing.values()].sort((a, b) => a.name.localeCompare(b.name)),
|
|
423
|
+
};
|
|
424
|
+
writeFileSync(indexPath, stringifyYaml(index), 'utf-8');
|
|
425
|
+
return indexPath;
|
|
426
|
+
}
|
|
427
|
+
/** Get all build results. */
|
|
428
|
+
getResults() {
|
|
429
|
+
return this.results;
|
|
430
|
+
}
|
|
431
|
+
/** Print summary table of build results. */
|
|
432
|
+
printSummary() {
|
|
433
|
+
const succeeded = this.results.filter(r => r.success);
|
|
434
|
+
const failed = this.results.filter(r => !r.success);
|
|
435
|
+
this.info(`\n${'─'.repeat(60)}`);
|
|
436
|
+
this.info(`Registry build complete`);
|
|
437
|
+
this.info(` Succeeded: ${succeeded.length}`);
|
|
438
|
+
this.info(` Failed: ${failed.length}`);
|
|
439
|
+
if (failed.length > 0) {
|
|
440
|
+
this.info(`\nFailed packages:`);
|
|
441
|
+
for (const r of failed) {
|
|
442
|
+
this.info(` ${r.name}: ${r.error}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const totalDuration = this.results.reduce((sum, r) => sum + r.durationMs, 0);
|
|
446
|
+
this.info(`\nTotal time: ${(totalDuration / 1000).toFixed(1)}s`);
|
|
447
|
+
}
|
|
448
|
+
// ── Private ────────────────────────────────────────────
|
|
449
|
+
getManifestDir(name, version) {
|
|
450
|
+
return join(this.options.registryDir, name, version);
|
|
451
|
+
}
|
|
452
|
+
recordResult(name, version, success, error, startTime, manifestPath) {
|
|
453
|
+
const result = {
|
|
454
|
+
name,
|
|
455
|
+
version,
|
|
456
|
+
success,
|
|
457
|
+
manifestPath,
|
|
458
|
+
error,
|
|
459
|
+
durationMs: Date.now() - startTime,
|
|
460
|
+
};
|
|
461
|
+
this.results.push(result);
|
|
462
|
+
return result;
|
|
463
|
+
}
|
|
464
|
+
async analyzeAndGenerate(packageName, version, packageDir, sourceType, entryPoint) {
|
|
465
|
+
// Find orchestrator binary
|
|
466
|
+
let orchestratorBinary = findOrchestratorBinary();
|
|
467
|
+
if (!orchestratorBinary) {
|
|
468
|
+
const downloaded = await ensureBinary('grafema-orchestrator', null, this.debug);
|
|
469
|
+
if (downloaded)
|
|
470
|
+
orchestratorBinary = downloaded;
|
|
471
|
+
}
|
|
472
|
+
if (!orchestratorBinary) {
|
|
473
|
+
throw new Error('grafema-orchestrator binary not found');
|
|
474
|
+
}
|
|
475
|
+
// Create temp workspace (short path to avoid Unix socket 104-byte limit)
|
|
476
|
+
const shortName = packageName.replace(/^@/, '').replace(/[/@]/g, '-').slice(0, 20);
|
|
477
|
+
const tmpBase = join(tmpdir(), `gr-${shortName}-${Date.now() % 100000}`);
|
|
478
|
+
const tmpGrafemaDir = join(tmpBase, '.grafema');
|
|
479
|
+
const tmpDbPath = join(tmpGrafemaDir, 'graph.rfdb');
|
|
480
|
+
mkdirSync(tmpGrafemaDir, { recursive: true });
|
|
481
|
+
// Copy package to temp directory (avoids Rust glob issues with pnpm store paths
|
|
482
|
+
// that contain @, +, and other special characters)
|
|
483
|
+
const tmpPkgDir = join(tmpBase, 'pkg');
|
|
484
|
+
cpSync(packageDir, tmpPkgDir, { recursive: true, filter: (src) => {
|
|
485
|
+
// Skip node_modules WITHIN the package and .map files to save space
|
|
486
|
+
// The src path starts with packageDir — only check the relative suffix
|
|
487
|
+
const rel = src.slice(packageDir.length);
|
|
488
|
+
if (rel.includes('node_modules'))
|
|
489
|
+
return false;
|
|
490
|
+
if (rel.endsWith('.map'))
|
|
491
|
+
return false;
|
|
492
|
+
return true;
|
|
493
|
+
} });
|
|
494
|
+
// Determine include patterns based on source type.
|
|
495
|
+
// For compiled_js: include .mjs alongside .js/.cjs (e.g., tsx's loader.mjs as entrypoint)
|
|
496
|
+
const includePatterns = sourceType === 'compiled_js'
|
|
497
|
+
? ['**/*.js', '**/*.cjs', '**/*.mjs']
|
|
498
|
+
: ['**/*.ts', '**/*.js', '**/*.mjs'];
|
|
499
|
+
const excludePatterns = [
|
|
500
|
+
'**/node_modules/**', '**/*.test.*', '**/*.spec.*',
|
|
501
|
+
'**/test/**', '**/tests/**',
|
|
502
|
+
];
|
|
503
|
+
// Count JS files to compute adaptive timeout
|
|
504
|
+
const fileCount = countFilesRecursive(tmpPkgDir, includePatterns, excludePatterns);
|
|
505
|
+
// Base 60s + 2s per file, minimum 60s, capped at user's max timeout
|
|
506
|
+
const adaptiveTimeout = Math.min(Math.max(60_000, 60_000 + fileCount * 2_000), this.options.timeout);
|
|
507
|
+
this.debug(` Files: ~${fileCount}, timeout: ${(adaptiveTimeout / 1000).toFixed(0)}s`);
|
|
508
|
+
// Write temp config
|
|
509
|
+
const configContent = stringifyYaml({
|
|
510
|
+
root: tmpPkgDir,
|
|
511
|
+
include: includePatterns,
|
|
512
|
+
exclude: excludePatterns,
|
|
513
|
+
plugins: [],
|
|
514
|
+
});
|
|
515
|
+
const configPath = join(tmpBase, 'grafema.config.yaml');
|
|
516
|
+
writeFileSync(configPath, configContent, 'utf-8');
|
|
517
|
+
// Start temp RFDB
|
|
518
|
+
const backend = new RFDBServerBackend({
|
|
519
|
+
dbPath: tmpDbPath,
|
|
520
|
+
autoStart: true,
|
|
521
|
+
silent: !this.options.verbose,
|
|
522
|
+
clientName: 'registry',
|
|
523
|
+
});
|
|
524
|
+
try {
|
|
525
|
+
await backend.connect();
|
|
526
|
+
// Spawn orchestrator
|
|
527
|
+
const exitCode = await this.spawnOrchestrator(orchestratorBinary, configPath, backend.socketPath, adaptiveTimeout);
|
|
528
|
+
if (exitCode !== 0) {
|
|
529
|
+
throw new Error(`Orchestrator exited with code ${exitCode}`);
|
|
530
|
+
}
|
|
531
|
+
// Resolve effects-db path
|
|
532
|
+
const effectsDbPath = this.resolveEffectsDbPath();
|
|
533
|
+
// Normalize entry point (strip leading ./, ensure extension)
|
|
534
|
+
let entryFile = entryPoint?.replace(/^\.\//, '');
|
|
535
|
+
if (entryFile && !/\.\w+$/.test(entryFile)) {
|
|
536
|
+
// No extension (e.g., "index") — append .js for compiled packages
|
|
537
|
+
entryFile = `${entryFile}.js`;
|
|
538
|
+
}
|
|
539
|
+
// Generate manifest
|
|
540
|
+
const purl = `pkg:npm/${packageName}@${version}`;
|
|
541
|
+
const generator = new ManifestGenerator(backend, {
|
|
542
|
+
purl,
|
|
543
|
+
effectsDbPath,
|
|
544
|
+
grafemaDir: tmpGrafemaDir,
|
|
545
|
+
sourceType,
|
|
546
|
+
entryFile,
|
|
547
|
+
});
|
|
548
|
+
return await generator.generate();
|
|
549
|
+
}
|
|
550
|
+
finally {
|
|
551
|
+
// Cleanup
|
|
552
|
+
if (backend.connected) {
|
|
553
|
+
await backend.close();
|
|
554
|
+
}
|
|
555
|
+
// Remove temp directory
|
|
556
|
+
try {
|
|
557
|
+
rmSync(tmpBase, { recursive: true, force: true });
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
// Best-effort cleanup
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
spawnOrchestrator(binary, configPath, socketPath, timeout) {
|
|
565
|
+
const effectiveTimeout = timeout ?? this.options.timeout;
|
|
566
|
+
return new Promise((resolvePromise, reject) => {
|
|
567
|
+
const args = ['analyze', '--config', configPath, '--socket', socketPath];
|
|
568
|
+
this.debug(` Spawning: ${binary} ${args.join(' ')}`);
|
|
569
|
+
const child = spawn(binary, args, {
|
|
570
|
+
stdio: ['ignore', this.options.verbose ? 'inherit' : 'ignore', this.options.verbose ? 'inherit' : 'ignore'],
|
|
571
|
+
env: {
|
|
572
|
+
...process.env,
|
|
573
|
+
RUST_LOG: this.options.verbose ? 'info' : 'warn',
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
const timer = setTimeout(() => {
|
|
577
|
+
child.kill('SIGKILL');
|
|
578
|
+
reject(new Error(`Analysis timed out after ${effectiveTimeout / 1000}s`));
|
|
579
|
+
}, effectiveTimeout);
|
|
580
|
+
child.on('error', (err) => {
|
|
581
|
+
clearTimeout(timer);
|
|
582
|
+
reject(new Error(`Failed to spawn orchestrator: ${err.message}`));
|
|
583
|
+
});
|
|
584
|
+
child.on('close', (code) => {
|
|
585
|
+
clearTimeout(timer);
|
|
586
|
+
resolvePromise(code ?? 1);
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
resolveEffectsDbPath() {
|
|
591
|
+
if (this.options.effectsDbPath && existsSync(this.options.effectsDbPath)) {
|
|
592
|
+
return this.options.effectsDbPath;
|
|
593
|
+
}
|
|
594
|
+
// Try common locations
|
|
595
|
+
const candidates = [
|
|
596
|
+
join(this.options.projectPath, 'effects-db'),
|
|
597
|
+
join(this.options.projectPath, '..', 'effects-db'),
|
|
598
|
+
];
|
|
599
|
+
return candidates.find(p => existsSync(p));
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
/** Count files matching include patterns (quick estimate for timeout calculation). */
|
|
603
|
+
function countFilesRecursive(dir, includes, _excludes) {
|
|
604
|
+
const extensions = new Set();
|
|
605
|
+
for (const pattern of includes) {
|
|
606
|
+
const match = pattern.match(/\*\.(\w+)$/);
|
|
607
|
+
if (match)
|
|
608
|
+
extensions.add(`.${match[1]}`);
|
|
609
|
+
}
|
|
610
|
+
if (extensions.size === 0)
|
|
611
|
+
return 0;
|
|
612
|
+
let count = 0;
|
|
613
|
+
const walk = (d) => {
|
|
614
|
+
try {
|
|
615
|
+
for (const entry of readdirSync(d, { withFileTypes: true })) {
|
|
616
|
+
const fullPath = join(d, entry.name);
|
|
617
|
+
if (entry.isDirectory()) {
|
|
618
|
+
// Skip excluded directories
|
|
619
|
+
if (entry.name === 'node_modules' || entry.name === 'test' || entry.name === 'tests')
|
|
620
|
+
continue;
|
|
621
|
+
walk(fullPath);
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
const ext = entry.name.slice(entry.name.lastIndexOf('.'));
|
|
625
|
+
if (extensions.has(ext) && !entry.name.includes('.test.') && !entry.name.includes('.spec.')) {
|
|
626
|
+
count++;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
catch {
|
|
632
|
+
// Ignore permission errors
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
walk(dir);
|
|
636
|
+
return count;
|
|
637
|
+
}
|
|
638
|
+
//# sourceMappingURL=registry.js.map
|