@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.
Files changed (49) hide show
  1. package/dist/core/FileOverview.d.ts.map +1 -1
  2. package/dist/core/FileOverview.js +16 -12
  3. package/dist/core/FileOverview.js.map +1 -1
  4. package/dist/federation/FederatedRouter.d.ts +124 -0
  5. package/dist/federation/FederatedRouter.d.ts.map +1 -0
  6. package/dist/federation/FederatedRouter.js +297 -0
  7. package/dist/federation/FederatedRouter.js.map +1 -0
  8. package/dist/federation/ShardDiscovery.d.ts +56 -0
  9. package/dist/federation/ShardDiscovery.d.ts.map +1 -0
  10. package/dist/federation/ShardDiscovery.js +100 -0
  11. package/dist/federation/ShardDiscovery.js.map +1 -0
  12. package/dist/federation/index.d.ts +28 -0
  13. package/dist/federation/index.d.ts.map +1 -0
  14. package/dist/federation/index.js +26 -0
  15. package/dist/federation/index.js.map +1 -0
  16. package/dist/index.d.ts +4 -2
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +3 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/manifest/generator.d.ts.map +1 -1
  21. package/dist/manifest/generator.js +38 -5
  22. package/dist/manifest/generator.js.map +1 -1
  23. package/dist/manifest/index.d.ts +2 -0
  24. package/dist/manifest/index.d.ts.map +1 -1
  25. package/dist/manifest/index.js +1 -0
  26. package/dist/manifest/index.js.map +1 -1
  27. package/dist/manifest/registry.d.ts +116 -0
  28. package/dist/manifest/registry.d.ts.map +1 -0
  29. package/dist/manifest/registry.js +638 -0
  30. package/dist/manifest/registry.js.map +1 -0
  31. package/dist/manifest/resolver.d.ts +9 -0
  32. package/dist/manifest/resolver.d.ts.map +1 -1
  33. package/dist/manifest/resolver.js +31 -0
  34. package/dist/manifest/resolver.js.map +1 -1
  35. package/dist/notation/traceRenderer.d.ts +2 -0
  36. package/dist/notation/traceRenderer.d.ts.map +1 -1
  37. package/dist/notation/traceRenderer.js +6 -5
  38. package/dist/notation/traceRenderer.js.map +1 -1
  39. package/package.json +3 -3
  40. package/src/core/FileOverview.ts +16 -11
  41. package/src/federation/FederatedRouter.ts +440 -0
  42. package/src/federation/ShardDiscovery.ts +130 -0
  43. package/src/federation/index.ts +35 -0
  44. package/src/index.ts +16 -1
  45. package/src/manifest/generator.ts +37 -5
  46. package/src/manifest/index.ts +2 -0
  47. package/src/manifest/registry.ts +769 -0
  48. package/src/manifest/resolver.ts +33 -0
  49. 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