@suss/adapter-typescript 0.15.0 → 0.16.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/dist/index.cjs CHANGED
@@ -67,7 +67,7 @@ __export(index_exports, {
67
67
  module.exports = __toCommonJS(index_exports);
68
68
 
69
69
  // src/adapter.ts
70
- var import_node_path9 = __toESM(require("path"), 1);
70
+ var import_node_path10 = __toESM(require("path"), 1);
71
71
  var import_ts_morph60 = require("ts-morph");
72
72
  var import_behavioral_ir8 = require("@suss/behavioral-ir");
73
73
  var import_datalog6 = require("@suss/datalog");
@@ -1864,8 +1864,8 @@ function loadImportGraphsDepthFirstFromPaths(project, rootPaths) {
1864
1864
  stack.pop();
1865
1865
  loadOrder.push(top.path);
1866
1866
  let deepest = 0;
1867
- for (const path12 of top.deps) {
1868
- deepest = Math.max(deepest, chainDepth.get(path12) ?? 0);
1867
+ for (const path13 of top.deps) {
1868
+ deepest = Math.max(deepest, chainDepth.get(path13) ?? 0);
1869
1869
  }
1870
1870
  chainDepth.set(top.path, deepest + 1);
1871
1871
  }
@@ -2210,11 +2210,11 @@ function walkAndWarm(root, reach) {
2210
2210
  if (warmedFiles.get(target) !== void 0) {
2211
2211
  continue;
2212
2212
  }
2213
- const path12 = target.getFilePath();
2214
- if (visited.has(path12)) {
2213
+ const path13 = target.getFilePath();
2214
+ if (visited.has(path13)) {
2215
2215
  continue;
2216
2216
  }
2217
- visited.add(path12);
2217
+ visited.add(path13);
2218
2218
  stack.push(frameOf(target, reach));
2219
2219
  continue;
2220
2220
  }
@@ -2231,7 +2231,7 @@ function sitsOnWarmChains(file) {
2231
2231
  return false;
2232
2232
  }
2233
2233
  const imported = importedFilePathsOf(project, file.getFilePath());
2234
- return imported.length > 0 && imported.every((path12) => warmed.has(path12));
2234
+ return imported.length > 0 && imported.every((path13) => warmed.has(path13));
2235
2235
  }
2236
2236
  function markWarmed(file) {
2237
2237
  warmedFiles.set(file, true);
@@ -5848,9 +5848,9 @@ function createSourceFileLookup(project) {
5848
5848
  if (sf === null) {
5849
5849
  return null;
5850
5850
  }
5851
- const path12 = sf.getFilePath();
5852
- const index = functionsByFile.get(path12) ?? indexFunctions(sf);
5853
- functionsByFile.set(path12, index);
5851
+ const path13 = sf.getFilePath();
5852
+ const index = functionsByFile.get(path13) ?? indexFunctions(sf);
5853
+ functionsByFile.set(path13, index);
5854
5854
  return index.get(rangeKey(location.range)) ?? null;
5855
5855
  }
5856
5856
  };
@@ -6377,7 +6377,7 @@ function extractEndpointContract(endpointNode, pattern, framework) {
6377
6377
  }
6378
6378
  const responses = [];
6379
6379
  let method;
6380
- let path12;
6380
+ let path13;
6381
6381
  for (const prop of endpointNode.getProperties()) {
6382
6382
  if (!import_ts_morph28.Node.isPropertyAssignment(prop)) {
6383
6383
  continue;
@@ -6392,7 +6392,7 @@ function extractEndpointContract(endpointNode, pattern, framework) {
6392
6392
  if (propName === pattern.pathProperty) {
6393
6393
  const val = prop.getInitializer();
6394
6394
  if (val !== void 0 && import_ts_morph28.Node.isStringLiteral(val)) {
6395
- path12 = val.getLiteralValue();
6395
+ path13 = val.getLiteralValue();
6396
6396
  }
6397
6397
  }
6398
6398
  if (propName === pattern.responseExtraction.property) {
@@ -6416,10 +6416,10 @@ function extractEndpointContract(endpointNode, pattern, framework) {
6416
6416
  if (responses.length === 0) {
6417
6417
  return null;
6418
6418
  }
6419
- const boundaryBinding = method !== void 0 || path12 !== void 0 ? (0, import_behavioral_ir2.restBinding)({
6419
+ const boundaryBinding = method !== void 0 || path13 !== void 0 ? (0, import_behavioral_ir2.restBinding)({
6420
6420
  transport: "http",
6421
6421
  method: method ?? null,
6422
- path: path12 ?? null,
6422
+ path: path13 ?? null,
6423
6423
  recognition: framework
6424
6424
  }) : null;
6425
6425
  return {
@@ -8334,7 +8334,34 @@ function joinRoutePath(prefix, suffix) {
8334
8334
  }
8335
8335
  return `/${segments.join("/")}`;
8336
8336
  }
8337
- function discoverDecoratedRoutes(sourceFile, match, kind, resolution) {
8337
+ function declaredChannelOf(source, decorator, resolution) {
8338
+ if (source.from === "literal") {
8339
+ return source.value;
8340
+ }
8341
+ if (source.from === "unstated") {
8342
+ return null;
8343
+ }
8344
+ if (!import_ts_morph38.Node.isDecorator(decorator)) {
8345
+ return null;
8346
+ }
8347
+ const arg = decorator.getArguments()[source.position];
8348
+ if (arg === void 0) {
8349
+ return null;
8350
+ }
8351
+ const text = literalStringOf(arg, resolution);
8352
+ return text === null || text === "" ? null : text;
8353
+ }
8354
+ function literalStringOf(node, resolution) {
8355
+ if (import_ts_morph38.Node.isStringLiteral(node) || import_ts_morph38.Node.isNoSubstitutionTemplateLiteral(node)) {
8356
+ return node.getLiteralValue();
8357
+ }
8358
+ const written = resolution?.resolveWrittenValue(node) ?? null;
8359
+ if (written === null || written === node) {
8360
+ return null;
8361
+ }
8362
+ return literalStringOf(written, resolution);
8363
+ }
8364
+ function discoverDecoratedRoutes(sourceFile, match, kind, resolution, binding) {
8338
8365
  const acceptedModules = Array.isArray(match.importModule) ? match.importModule : [match.importModule];
8339
8366
  const routeDecoratorNames = Object.keys(match.methodDecoratorRouteMap);
8340
8367
  const importedRouteDecorators = /* @__PURE__ */ new Set();
@@ -8366,6 +8393,22 @@ function discoverDecoratedRoutes(sourceFile, match, kind, resolution) {
8366
8393
  const pathPrefix = routePathOf(marker.args, resolution);
8367
8394
  const className = cls.getName() ?? "<anon-class>";
8368
8395
  for (const handler of decoratedCallablesOf(cls, routeDecoratorNames)) {
8396
+ if (binding !== void 0) {
8397
+ results.push({
8398
+ func: handler.func,
8399
+ kind,
8400
+ name: `${className}.${handler.name}`,
8401
+ channelInfo: {
8402
+ messageBus: binding.messageBus,
8403
+ channel: declaredChannelOf(
8404
+ binding.channel,
8405
+ handler.decorator,
8406
+ resolution
8407
+ )
8408
+ }
8409
+ });
8410
+ continue;
8411
+ }
8369
8412
  const httpMethod = match.methodDecoratorRouteMap[handler.standsFor];
8370
8413
  const pathSuffix = resolveRoutePathArg(handler.decorator, resolution);
8371
8414
  const routePath = joinRoutePath(pathPrefix, pathSuffix);
@@ -9379,7 +9422,7 @@ function attributeValueOf(node, name) {
9379
9422
  }
9380
9423
  return null;
9381
9424
  }
9382
- function literalStringOf(value) {
9425
+ function literalStringOf2(value) {
9383
9426
  const unwrapped2 = import_ts_morph43.Node.isJsxExpression(value) ? value.getExpression() ?? value : value;
9384
9427
  if (import_ts_morph43.Node.isStringLiteral(unwrapped2) || import_ts_morph43.Node.isNoSubstitutionTemplateLiteral(unwrapped2)) {
9385
9428
  return unwrapped2.getLiteralValue();
@@ -9389,7 +9432,7 @@ function literalStringOf(value) {
9389
9432
  function ownPathOfJsx(node, match) {
9390
9433
  const pathValue = attributeValueOf(node, match.pathAttribute);
9391
9434
  if (pathValue !== null) {
9392
- const literal = literalStringOf(pathValue);
9435
+ const literal = literalStringOf2(pathValue);
9393
9436
  return literal !== null ? { kind: "literal", value: literal } : { kind: "unreadable" };
9394
9437
  }
9395
9438
  if (match.indexAttribute !== void 0 && attributeValueOf(node, match.indexAttribute) !== null) {
@@ -9480,14 +9523,14 @@ function unitForJsxRoute(node, match, routeNames, kind, resolution) {
9480
9523
  }
9481
9524
  return routeUnit(node, kind, match.method, resolved.path, target, resolution);
9482
9525
  }
9483
- function routeUnit(at, kind, method, path12, target, resolution) {
9484
- const routeInfo = { method, path: path12 };
9526
+ function routeUnit(at, kind, method, path13, target, resolution) {
9527
+ const routeInfo = { method, path: path13 };
9485
9528
  if (target.kind === "unreadable") {
9486
9529
  return {
9487
9530
  func: null,
9488
9531
  announcedAt: at,
9489
9532
  kind,
9490
- name: path12,
9533
+ name: path13,
9491
9534
  nameKind: "label",
9492
9535
  routeInfo
9493
9536
  };
@@ -9657,7 +9700,7 @@ function unitsUnderRouteObject(object, match, kind, resolution, prefix, ancestry
9657
9700
  function ownPathOfObject(object, match) {
9658
9701
  const pathValue = routePropertyOf(object, match.pathAttribute);
9659
9702
  if (pathValue !== null) {
9660
- const literal = literalStringOf(pathValue);
9703
+ const literal = literalStringOf2(pathValue);
9661
9704
  return literal !== null ? { kind: "literal", value: literal } : { kind: "unreadable" };
9662
9705
  }
9663
9706
  if (match.indexAttribute !== void 0) {
@@ -9917,6 +9960,9 @@ function resolvePackageExportsCached(packageJsonPath) {
9917
9960
  return fresh;
9918
9961
  }
9919
9962
  function discoverPackageExports(sourceFile, match, kind) {
9963
+ if (match.packageJsonPath === void 0) {
9964
+ return [];
9965
+ }
9920
9966
  const { entries } = resolvePackageExportsCached(match.packageJsonPath);
9921
9967
  const filePath = sourceFile.getFilePath();
9922
9968
  const matching = [];
@@ -10304,12 +10350,12 @@ function originEndOf(flattened) {
10304
10350
  const slash = flattened.indexOf("/", opener[0].length);
10305
10351
  return slash === -1 ? flattened.length : slash;
10306
10352
  }
10307
- function appendPathText(path12, text) {
10353
+ function appendPathText(path13, text) {
10308
10354
  const idx = text.search(/[?#]/);
10309
10355
  if (idx === -1) {
10310
- return { path: path12 + text, stop: false };
10356
+ return { path: path13 + text, stop: false };
10311
10357
  }
10312
- return { path: path12 + text.slice(0, idx), stop: true };
10358
+ return { path: path13 + text.slice(0, idx), stop: true };
10313
10359
  }
10314
10360
  function parseAbsoluteUrl(text) {
10315
10361
  try {
@@ -10333,8 +10379,8 @@ function pathnameOfAbsoluteLiteral(text) {
10333
10379
  return stripQueryAndFragment(stripOriginManually(text));
10334
10380
  }
10335
10381
  function pathFromLiteralUrl(text) {
10336
- const path12 = isAbsoluteUrlLiteral(text) ? pathnameOfAbsoluteLiteral(text) : stripQueryAndFragment(text);
10337
- return path12 === "" ? void 0 : path12;
10382
+ const path13 = isAbsoluteUrlLiteral(text) ? pathnameOfAbsoluteLiteral(text) : stripQueryAndFragment(text);
10383
+ return path13 === "" ? void 0 : path13;
10338
10384
  }
10339
10385
  function pathFromTemplateUrl(arg, resolution) {
10340
10386
  const headText = arg.getHead().getLiteralText();
@@ -10342,7 +10388,7 @@ function pathFromTemplateUrl(arg, resolution) {
10342
10388
  const tails = spans.map((span) => span.getLiteral().getLiteralText());
10343
10389
  const originEnd = originEndOf([headText, ...tails].join(SUBSTITUTION));
10344
10390
  const head = appendPathText("", headText.slice(originEnd));
10345
- let path12 = head.path;
10391
+ let path13 = head.path;
10346
10392
  let stop = head.stop;
10347
10393
  let at = headText.length;
10348
10394
  for (let i = 0; i < spans.length && !stop; i++) {
@@ -10352,18 +10398,18 @@ function pathFromTemplateUrl(arg, resolution) {
10352
10398
  continue;
10353
10399
  }
10354
10400
  if (at >= originEnd) {
10355
- path12 += holeText(span.getExpression(), resolution);
10401
+ path13 += holeText(span.getExpression(), resolution);
10356
10402
  }
10357
10403
  at += SUBSTITUTION.length;
10358
10404
  const appended = appendPathText(
10359
- path12,
10405
+ path13,
10360
10406
  tailText.slice(Math.max(0, originEnd - at))
10361
10407
  );
10362
- path12 = appended.path;
10408
+ path13 = appended.path;
10363
10409
  stop = appended.stop;
10364
10410
  at += tailText.length;
10365
10411
  }
10366
- return path12 === "" ? void 0 : path12;
10412
+ return path13 === "" ? void 0 : path13;
10367
10413
  }
10368
10414
  function pathFromUrlNode(node, resolution) {
10369
10415
  if (import_ts_morph48.Node.isStringLiteral(node)) {
@@ -10530,9 +10576,9 @@ function withMountPrefix(routeInfo, subjectNode, mountPrefixes) {
10530
10576
  const prefix = mountPrefixes.effectivePrefixFor(subjectNode);
10531
10577
  return prefix === "" ? routeInfo : { ...routeInfo, path: joinMountedPath(prefix, routeInfo.path) };
10532
10578
  }
10533
- function joinMountedPath(prefix, path12) {
10579
+ function joinMountedPath(prefix, path13) {
10534
10580
  const trimmed = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
10535
- return trimmed === "" ? path12 : trimmed + path12;
10581
+ return trimmed === "" ? path13 : trimmed + path13;
10536
10582
  }
10537
10583
  function registrationSubjectsOf(sourceFile, importModule, importName) {
10538
10584
  let importedLocalName = null;
@@ -10696,11 +10742,11 @@ function extractRouteInfoFromBinding(call, methodName, binding, resolution) {
10696
10742
  return null;
10697
10743
  }
10698
10744
  const method2 = stringProperty(routeObject, binding.method.property);
10699
- const path13 = stringProperty(routeObject, binding.path.property);
10700
- if (method2 === null || path13 === null) {
10745
+ const path14 = stringProperty(routeObject, binding.path.property);
10746
+ if (method2 === null || path14 === null) {
10701
10747
  return null;
10702
10748
  }
10703
- return { method: method2.toUpperCase(), path: path13 };
10749
+ return { method: method2.toUpperCase(), path: path14 };
10704
10750
  }
10705
10751
  if (binding.method.type !== "fromRegistration" || binding.path.type !== "fromArgument") {
10706
10752
  return null;
@@ -10717,8 +10763,8 @@ function extractRouteInfoFromBinding(call, methodName, binding, resolution) {
10717
10763
  method = arg.getLiteralValue().toUpperCase();
10718
10764
  }
10719
10765
  const pathArg = call.getArguments()[binding.path.position];
10720
- const path12 = pathArg === void 0 ? void 0 : pathFromArgument(pathArg, resolution);
10721
- return path12 === void 0 ? null : { method, path: path12 };
10766
+ const path13 = pathArg === void 0 ? void 0 : pathFromArgument(pathArg, resolution);
10767
+ return path13 === void 0 ? null : { method, path: path13 };
10722
10768
  }
10723
10769
 
10724
10770
  // src/discovery/registrationLoop.ts
@@ -10841,7 +10887,7 @@ function bodyCallsOn(loop, routables) {
10841
10887
  }
10842
10888
  function readRouteSpec(element, shape, resolution) {
10843
10889
  let method = null;
10844
- let path12 = null;
10890
+ let path13 = null;
10845
10891
  let handler = null;
10846
10892
  for (const prop of element.getProperties()) {
10847
10893
  if (import_ts_morph50.Node.isMethodDeclaration(prop) && prop.getName() === shape.handlerKey) {
@@ -10859,16 +10905,16 @@ function readRouteSpec(element, shape, resolution) {
10859
10905
  if (name === shape.methodKey) {
10860
10906
  method = readStringLiteralValue(init);
10861
10907
  } else if (name === shape.pathKey) {
10862
- path12 = readStringLiteralValue(init);
10908
+ path13 = readStringLiteralValue(init);
10863
10909
  } else if (name === shape.handlerKey) {
10864
10910
  const func = functionValueOf(init, resolution);
10865
10911
  handler = func === null ? null : { func, name: handlerName(init) };
10866
10912
  }
10867
10913
  }
10868
- if (method === null || path12 === null || handler === null) {
10914
+ if (method === null || path13 === null || handler === null) {
10869
10915
  return null;
10870
10916
  }
10871
- return { method, path: path12, handler: handler.func, handlerName: handler.name };
10917
+ return { method, path: path13, handler: handler.func, handlerName: handler.name };
10872
10918
  }
10873
10919
  function readStringLiteralValue(node) {
10874
10920
  if (import_ts_morph50.Node.isStringLiteral(node) || import_ts_morph50.Node.isNoSubstitutionTemplateLiteral(node)) {
@@ -10897,8 +10943,8 @@ function discoverRegistrationTemplates(sourceFile, match, kind, resolution) {
10897
10943
  }
10898
10944
  const args = node.getArguments();
10899
10945
  for (const reg of match.registrations) {
10900
- const path12 = substitutePath(reg.pathTemplate, args);
10901
- if (path12 === null) {
10946
+ const path13 = substitutePath(reg.pathTemplate, args);
10947
+ if (path13 === null) {
10902
10948
  continue;
10903
10949
  }
10904
10950
  const handler = resolveHandler(reg.handlerArg, args, resolution);
@@ -10911,7 +10957,7 @@ function discoverRegistrationTemplates(sourceFile, match, kind, resolution) {
10911
10957
  name: handler.name,
10912
10958
  routeInfo: {
10913
10959
  method: reg.method.toUpperCase(),
10914
- path: path12
10960
+ path: path13
10915
10961
  }
10916
10962
  });
10917
10963
  }
@@ -11237,7 +11283,8 @@ function runPattern(sourceFile, pattern, resolution, mountPrefixes) {
11237
11283
  sourceFile,
11238
11284
  pattern.match,
11239
11285
  pattern.kind,
11240
- resolution
11286
+ resolution,
11287
+ pattern.binding
11241
11288
  );
11242
11289
  }
11243
11290
  if (pattern.match.type === "jsxElementRoute") {
@@ -12502,7 +12549,38 @@ var REACHABLE_RULES = [
12502
12549
  )
12503
12550
  ];
12504
12551
  var NO_RECOGNIZERS = { invocation: [], access: [] };
12505
- function expandReachableClosure(seeds, project, options, projectFileSet, facts, recognizers = NO_RECOGNIZERS, alreadySummarized = []) {
12552
+ function recognizerOnlyRoots(packsByFile) {
12553
+ const roots = [];
12554
+ for (const [sourceFile, packs] of packsByFile) {
12555
+ const recognizerOnly = packs.some(
12556
+ (pack) => pack.discovery.length === 0 && pack.discoverUnits === void 0 && ((pack.invocationRecognizers?.length ?? 0) > 0 || (pack.accessRecognizers?.length ?? 0) > 0)
12557
+ );
12558
+ if (!recognizerOnly) {
12559
+ continue;
12560
+ }
12561
+ for (const declaration of sourceFile.getFunctions()) {
12562
+ if (declaration.isExported() && hasBody(declaration)) {
12563
+ roots.push({
12564
+ func: declaration,
12565
+ name: declaration.getName() ?? "<anon>"
12566
+ });
12567
+ }
12568
+ }
12569
+ for (const variableStatement of sourceFile.getVariableStatements()) {
12570
+ if (!variableStatement.isExported()) {
12571
+ continue;
12572
+ }
12573
+ for (const declaration of variableStatement.getDeclarations()) {
12574
+ const init = declaration.getInitializer();
12575
+ if (init !== void 0 && (import_ts_morph56.Node.isArrowFunction(init) || import_ts_morph56.Node.isFunctionExpression(init)) && hasBody(init)) {
12576
+ roots.push({ func: init, name: declaration.getName() });
12577
+ }
12578
+ }
12579
+ }
12580
+ }
12581
+ return roots;
12582
+ }
12583
+ function expandReachableClosure(seeds, project, options, projectFileSet, facts, recognizers = NO_RECOGNIZERS, alreadySummarized = [], extraRoots = []) {
12506
12584
  const lookup = createSourceFileLookup(project);
12507
12585
  const db = facts?.db ?? new import_datalog4.Database();
12508
12586
  const functionByKey = /* @__PURE__ */ new Map();
@@ -12522,6 +12600,13 @@ function expandReachableClosure(seeds, project, options, projectFileSet, facts,
12522
12600
  db.add("entry", [key]);
12523
12601
  }
12524
12602
  }
12603
+ for (const root of extraRoots) {
12604
+ const key = nodeKey2(root.func);
12605
+ if (!functionByKey.has(key)) {
12606
+ functionByKey.set(key, root);
12607
+ }
12608
+ db.add("entry", [key]);
12609
+ }
12525
12610
  const knownKeys = /* @__PURE__ */ new Set();
12526
12611
  for (const summary of alreadySummarized) {
12527
12612
  const func = lookup.functionAt(summary.location);
@@ -13583,6 +13668,185 @@ function computeAdapterPacksDigest(packVersions) {
13583
13668
  return `${adapterStamp}|${sortedPacks.join(",")}`;
13584
13669
  }
13585
13670
 
13671
+ // src/workspacePatterns.ts
13672
+ var import_node_crypto4 = require("crypto");
13673
+ var import_node_fs6 = __toESM(require("fs"), 1);
13674
+ var import_node_path9 = __toESM(require("path"), 1);
13675
+ function readJson(filePath) {
13676
+ try {
13677
+ return JSON.parse(import_node_fs6.default.readFileSync(filePath, "utf8"));
13678
+ } catch {
13679
+ return void 0;
13680
+ }
13681
+ }
13682
+ function pnpmWorkspaceGlobs(yamlText) {
13683
+ const globs = [];
13684
+ let inPackages = false;
13685
+ for (const line of yamlText.split("\n")) {
13686
+ if (/^packages\s*:/.test(line)) {
13687
+ inPackages = true;
13688
+ continue;
13689
+ }
13690
+ if (inPackages) {
13691
+ const item = line.match(/^\s+-\s+["']?([^"'#\s]+)["']?/);
13692
+ if (item !== null) {
13693
+ globs.push(item[1]);
13694
+ continue;
13695
+ }
13696
+ if (/^\S/.test(line)) {
13697
+ inPackages = false;
13698
+ }
13699
+ }
13700
+ }
13701
+ return globs;
13702
+ }
13703
+ function workspaceGlobsAt(dir) {
13704
+ const pkg = readJson(import_node_path9.default.join(dir, "package.json"));
13705
+ if (typeof pkg === "object" && pkg !== null && "workspaces" in pkg) {
13706
+ const ws = pkg.workspaces;
13707
+ if (Array.isArray(ws)) {
13708
+ return ws.filter((g) => typeof g === "string");
13709
+ }
13710
+ if (typeof ws === "object" && ws !== null && Array.isArray(ws.packages)) {
13711
+ return ws.packages.filter(
13712
+ (g) => typeof g === "string"
13713
+ );
13714
+ }
13715
+ }
13716
+ const pnpmPath = import_node_path9.default.join(dir, "pnpm-workspace.yaml");
13717
+ if (import_node_fs6.default.existsSync(pnpmPath)) {
13718
+ try {
13719
+ return pnpmWorkspaceGlobs(import_node_fs6.default.readFileSync(pnpmPath, "utf8"));
13720
+ } catch {
13721
+ return null;
13722
+ }
13723
+ }
13724
+ return null;
13725
+ }
13726
+ function isDirectory(candidate) {
13727
+ try {
13728
+ return import_node_fs6.default.statSync(candidate).isDirectory();
13729
+ } catch {
13730
+ return false;
13731
+ }
13732
+ }
13733
+ function subdirectoriesOf(dir) {
13734
+ try {
13735
+ return import_node_fs6.default.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).filter((name) => !name.startsWith(".") && name !== "node_modules");
13736
+ } catch {
13737
+ return [];
13738
+ }
13739
+ }
13740
+ function segmentToRegExp(segment) {
13741
+ const escaped = segment.replace(/[.+^${}()|[\]\\]/g, "\\$&");
13742
+ return new RegExp(`^${escaped.replace(/\*/g, "[^/]*")}$`);
13743
+ }
13744
+ function directoriesMatchingGlob(root, glob) {
13745
+ let current2 = [root];
13746
+ for (const segment of glob.split("/").filter((s) => s.length > 0)) {
13747
+ const next = [];
13748
+ if (segment.includes("*")) {
13749
+ const pattern = segmentToRegExp(segment);
13750
+ for (const dir of current2) {
13751
+ for (const entry of subdirectoriesOf(dir)) {
13752
+ if (pattern.test(entry)) {
13753
+ next.push(import_node_path9.default.join(dir, entry));
13754
+ }
13755
+ }
13756
+ }
13757
+ } else {
13758
+ for (const dir of current2) {
13759
+ next.push(import_node_path9.default.join(dir, segment));
13760
+ }
13761
+ }
13762
+ current2 = next.filter(isDirectory);
13763
+ }
13764
+ return current2;
13765
+ }
13766
+ function packageAt(dir) {
13767
+ const packageJsonPath = import_node_path9.default.join(dir, "package.json");
13768
+ const pkg = readJson(packageJsonPath);
13769
+ if (typeof pkg === "object" && pkg !== null && typeof pkg.name === "string") {
13770
+ return { name: pkg.name, packageJsonPath };
13771
+ }
13772
+ return null;
13773
+ }
13774
+ function workspacePackagesFrom(anchor) {
13775
+ let dir = import_node_path9.default.resolve(anchor);
13776
+ for (; ; ) {
13777
+ const globs = workspaceGlobsAt(dir);
13778
+ if (globs !== null) {
13779
+ const found = /* @__PURE__ */ new Map();
13780
+ for (const glob of globs) {
13781
+ if (glob.startsWith("!")) {
13782
+ continue;
13783
+ }
13784
+ for (const match of directoriesMatchingGlob(dir, glob)) {
13785
+ const pkg = packageAt(match);
13786
+ if (pkg !== null) {
13787
+ found.set(pkg.packageJsonPath, pkg);
13788
+ }
13789
+ }
13790
+ }
13791
+ return [...found.values()].sort(
13792
+ (a, b) => a.packageJsonPath.localeCompare(b.packageJsonPath)
13793
+ );
13794
+ }
13795
+ const parent = import_node_path9.default.dirname(dir);
13796
+ if (parent === dir) {
13797
+ return [];
13798
+ }
13799
+ dir = parent;
13800
+ }
13801
+ }
13802
+ function isWorkspaceMarked(pattern) {
13803
+ const match = pattern.match;
13804
+ return (match.type === "packageExports" || match.type === "packageImport") && match.workspaces === true;
13805
+ }
13806
+ function expandPattern(pattern, packages) {
13807
+ const match = pattern.match;
13808
+ if (match.type === "packageExports" && match.workspaces === true) {
13809
+ const { workspaces: _workspaces, ...rest } = match;
13810
+ return packages.map((pkg) => ({
13811
+ ...pattern,
13812
+ match: { ...rest, packageJsonPath: pkg.packageJsonPath }
13813
+ }));
13814
+ }
13815
+ if (match.type === "packageImport" && match.workspaces === true) {
13816
+ const listed = match.packages ?? [];
13817
+ const names = [.../* @__PURE__ */ new Set([...listed, ...packages.map((p) => p.name)])];
13818
+ if (names.length === 0) {
13819
+ return [];
13820
+ }
13821
+ return [{ ...pattern, match: { type: "packageImport", packages: names } }];
13822
+ }
13823
+ return [pattern];
13824
+ }
13825
+ function workspaceExpansionStamp(frameworks) {
13826
+ const paths = frameworks.flatMap((pack) => pack.discovery).flatMap(
13827
+ (pattern) => pattern.match.type === "packageExports" && pattern.match.packageJsonPath !== void 0 ? [pattern.match.packageJsonPath] : []
13828
+ ).sort();
13829
+ if (paths.length === 0) {
13830
+ return "none";
13831
+ }
13832
+ return (0, import_node_crypto4.createHash)("sha256").update(paths.join("\n")).digest("hex").slice(0, 12);
13833
+ }
13834
+ function expandWorkspacePatterns(frameworks, anchor) {
13835
+ const marked = frameworks.some(
13836
+ (pack) => pack.discovery.some(isWorkspaceMarked)
13837
+ );
13838
+ if (!marked) {
13839
+ return frameworks;
13840
+ }
13841
+ const packages = anchor !== void 0 ? workspacePackagesFrom(anchor) : [];
13842
+ return frameworks.map((pack) => ({
13843
+ ...pack,
13844
+ discovery: pack.discovery.flatMap(
13845
+ (pattern) => expandPattern(pattern, packages)
13846
+ )
13847
+ }));
13848
+ }
13849
+
13586
13850
  // src/adapter.ts
13587
13851
  var raise = (msg) => {
13588
13852
  throw new Error(msg);
@@ -13993,11 +14257,11 @@ function extractConsumerBinding(unit, pattern, pack, resolution) {
13993
14257
  return null;
13994
14258
  }
13995
14259
  const method = extractBindingMethod(binding, callSite, pack, resolution);
13996
- const path12 = extractBindingPath(binding, callSite, pack, resolution);
14260
+ const path13 = extractBindingPath(binding, callSite, pack, resolution);
13997
14261
  return (0, import_behavioral_ir8.restBinding)({
13998
14262
  transport: pack.protocol,
13999
14263
  method: method ?? null,
14000
- path: path12 ?? null,
14264
+ path: path13 ?? null,
14001
14265
  recognition: pack.name
14002
14266
  });
14003
14267
  }
@@ -14609,8 +14873,8 @@ function synthesizeCallerSummaries(wrapper, _project, options, resolution) {
14609
14873
  if (pathArg === void 0) {
14610
14874
  continue;
14611
14875
  }
14612
- const path12 = pathFromArgument(pathArg, resolution);
14613
- if (path12 === void 0) {
14876
+ const path13 = pathFromArgument(pathArg, resolution);
14877
+ if (path13 === void 0) {
14614
14878
  continue;
14615
14879
  }
14616
14880
  const callerFunc = enclosingFunction(callExpr);
@@ -14622,7 +14886,7 @@ function synthesizeCallerSummaries(wrapper, _project, options, resolution) {
14622
14886
  continue;
14623
14887
  }
14624
14888
  seen.add(dedupKey);
14625
- out.push(buildCallerSummary(wrapper, callerFunc, callExpr, path12, options));
14889
+ out.push(buildCallerSummary(wrapper, callerFunc, callExpr, path13, options));
14626
14890
  }
14627
14891
  return out;
14628
14892
  }
@@ -14679,7 +14943,7 @@ function callerName(func) {
14679
14943
  }
14680
14944
  return "anonymous";
14681
14945
  }
14682
- function buildCallerSummary(wrapper, callerFunc, callExpr, path12, options) {
14946
+ function buildCallerSummary(wrapper, callerFunc, callExpr, path13, options) {
14683
14947
  const wrapperBinding = wrapper.summary.identity.boundaryBinding;
14684
14948
  const wrapperRest = wrapperBinding?.semantics.name === "rest" ? wrapperBinding.semantics : null;
14685
14949
  const syntheticPack = {
@@ -14711,7 +14975,7 @@ function buildCallerSummary(wrapper, callerFunc, callExpr, path12, options) {
14711
14975
  raw.boundaryBinding = (0, import_behavioral_ir8.restBinding)({
14712
14976
  transport: wrapperBinding?.transport ?? "http",
14713
14977
  method: wrapperRest?.method ?? null,
14714
- path: path12,
14978
+ path: path13,
14715
14979
  recognition: wrapperBinding?.recognition ?? "unknown"
14716
14980
  });
14717
14981
  const summary = (0, import_extractor5.assembleSummary)(raw, options);
@@ -14777,25 +15041,32 @@ function loadRunFiles(project, candidatePaths, timer) {
14777
15041
  );
14778
15042
  return { sourceFiles, deep };
14779
15043
  }
14780
- function createTypeScriptAdapter(config) {
15044
+ function createTypeScriptAdapter(suppliedConfig) {
15045
+ const config = {
15046
+ ...suppliedConfig,
15047
+ frameworks: expandWorkspacePatterns(
15048
+ suppliedConfig.frameworks,
15049
+ suppliedConfig.projectRoot ?? (suppliedConfig.tsConfigFilePath !== void 0 ? import_node_path10.default.dirname(suppliedConfig.tsConfigFilePath) : void 0)
15050
+ )
15051
+ };
14781
15052
  const project = config.project ?? new import_ts_morph60.Project(
14782
15053
  config.tsConfigFilePath !== void 0 ? {
14783
15054
  tsConfigFilePath: config.tsConfigFilePath,
14784
15055
  skipAddingFilesFromTsConfig: true
14785
15056
  } : { skipAddingFilesFromTsConfig: true }
14786
15057
  );
14787
- const runRoot = config.projectRoot ?? (config.tsConfigFilePath !== void 0 ? workspaceRootFor(import_node_path9.default.dirname(config.tsConfigFilePath)) : void 0);
15058
+ const runRoot = config.projectRoot ?? (config.tsConfigFilePath !== void 0 ? workspaceRootFor(import_node_path10.default.dirname(config.tsConfigFilePath)) : void 0);
14788
15059
  let lazyBootstrapped = false;
14789
15060
  let projectFileSet;
14790
15061
  const cacheDir = declineWhenRunFromSource(
14791
- config.cacheDir === null ? null : config.cacheDir ?? (config.tsConfigFilePath !== void 0 ? import_node_path9.default.join(import_node_path9.default.dirname(config.tsConfigFilePath), ".suss", "cache") : null)
15062
+ config.cacheDir === null ? null : config.cacheDir ?? (config.tsConfigFilePath !== void 0 ? import_node_path10.default.join(import_node_path10.default.dirname(config.tsConfigFilePath), ".suss", "cache") : null)
14792
15063
  );
14793
15064
  const cache = createCacheLayer(cacheDir);
14794
15065
  const adapterPacksDigest = `${computeAdapterPacksDigest(
14795
15066
  config.frameworks.map(
14796
15067
  (p) => p.version !== void 0 ? { name: p.name, version: p.version } : { name: p.name }
14797
15068
  )
14798
- )}|${extractionConfigStamp(config)}`;
15069
+ )}|${extractionConfigStamp(config)}|ws:${workspaceExpansionStamp(config.frameworks)}`;
14799
15070
  const packWrappers = config.frameworks.flatMap(
14800
15071
  (pack) => pack.transparentWrappers ?? []
14801
15072
  );
@@ -15031,7 +15302,12 @@ function createTypeScriptAdapter(config) {
15031
15302
  },
15032
15303
  // Reached units the cache already serves emit nothing,
15033
15304
  // the way a cold run's seeds do not.
15034
- reused?.summaries ?? []
15305
+ reused?.summaries ?? [],
15306
+ // A recognizer-only pack's effects need a function to
15307
+ // live on even when nothing discovers units in its
15308
+ // files, so those files' exports join the walk as
15309
+ // roots.
15310
+ recognizerOnlyRoots(packsByFile)
15035
15311
  )
15036
15312
  ) : withSubUnits;
15037
15313
  const closureOwnership = caching && config.includeReachable !== false ? timer.time(
@@ -15428,8 +15704,8 @@ function buildSubUnitSummary(subUnit, parent, invocationRecognizers, accessRecog
15428
15704
  }
15429
15705
 
15430
15706
  // src/bootstrap/noTsconfigProject.ts
15431
- var import_node_fs6 = __toESM(require("fs"), 1);
15432
- var import_node_path10 = __toESM(require("path"), 1);
15707
+ var import_node_fs7 = __toESM(require("fs"), 1);
15708
+ var import_node_path11 = __toESM(require("path"), 1);
15433
15709
  var import_ts_morph61 = require("ts-morph");
15434
15710
  var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
15435
15711
  ".ts",
@@ -15460,7 +15736,7 @@ function findSourceFiles(rootDir, depth = 0) {
15460
15736
  const found = [];
15461
15737
  let entries;
15462
15738
  try {
15463
- entries = import_node_fs6.default.readdirSync(rootDir, { withFileTypes: true });
15739
+ entries = import_node_fs7.default.readdirSync(rootDir, { withFileTypes: true });
15464
15740
  } catch {
15465
15741
  return [];
15466
15742
  }
@@ -15468,10 +15744,10 @@ function findSourceFiles(rootDir, depth = 0) {
15468
15744
  if (entry.name.startsWith(".") || SKIP_DIRECTORIES.has(entry.name)) {
15469
15745
  continue;
15470
15746
  }
15471
- const full = import_node_path10.default.join(rootDir, entry.name);
15747
+ const full = import_node_path11.default.join(rootDir, entry.name);
15472
15748
  if (entry.isDirectory()) {
15473
15749
  found.push(...findSourceFiles(full, depth + 1));
15474
- } else if (SOURCE_EXTENSIONS.has(import_node_path10.default.extname(entry.name)) && !entry.name.endsWith(".d.ts")) {
15750
+ } else if (SOURCE_EXTENSIONS.has(import_node_path11.default.extname(entry.name)) && !entry.name.endsWith(".d.ts")) {
15475
15751
  found.push(full);
15476
15752
  }
15477
15753
  }
@@ -15506,15 +15782,15 @@ function createProjectWithoutTsconfig(rootDir) {
15506
15782
  return { project, files };
15507
15783
  }
15508
15784
  function findNearestTsconfig(startDir) {
15509
- let dir = import_node_path10.default.resolve(startDir);
15785
+ let dir = import_node_path11.default.resolve(startDir);
15510
15786
  while (true) {
15511
15787
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
15512
- const candidate = import_node_path10.default.join(dir, name);
15513
- if (import_node_fs6.default.existsSync(candidate)) {
15788
+ const candidate = import_node_path11.default.join(dir, name);
15789
+ if (import_node_fs7.default.existsSync(candidate)) {
15514
15790
  return candidate;
15515
15791
  }
15516
15792
  }
15517
- const parent = import_node_path10.default.dirname(dir);
15793
+ const parent = import_node_path11.default.dirname(dir);
15518
15794
  if (parent === dir) {
15519
15795
  return null;
15520
15796
  }
@@ -15523,7 +15799,7 @@ function findNearestTsconfig(startDir) {
15523
15799
  }
15524
15800
 
15525
15801
  // src/facts/why.ts
15526
- var import_node_path11 = __toESM(require("path"), 1);
15802
+ var import_node_path12 = __toESM(require("path"), 1);
15527
15803
  var import_ts_morph62 = require("ts-morph");
15528
15804
  var import_resolution2 = require("@suss/resolution");
15529
15805
  var JS_PHRASES = {
@@ -15536,7 +15812,7 @@ var WhySession = class {
15536
15812
  store;
15537
15813
  root;
15538
15814
  constructor(options) {
15539
- this.root = import_node_path11.default.resolve(options.dir);
15815
+ this.root = import_node_path12.default.resolve(options.dir);
15540
15816
  const tsconfig = findNearestTsconfig(this.root);
15541
15817
  this.project = tsconfig === null ? createProjectWithoutTsconfig(this.root).project : new import_ts_morph62.Project({ tsConfigFilePath: tsconfig });
15542
15818
  this.store = new ResolutionStore();
@@ -15625,7 +15901,7 @@ var WhySession = class {
15625
15901
  };
15626
15902
  }
15627
15903
  sourceFileAt(file) {
15628
- const absolute = import_node_path11.default.resolve(this.root, file);
15904
+ const absolute = import_node_path12.default.resolve(this.root, file);
15629
15905
  const loaded = this.project.getSourceFile(absolute);
15630
15906
  if (loaded !== void 0) {
15631
15907
  return loaded;
@@ -15639,7 +15915,7 @@ var WhySession = class {
15639
15915
  /** A module key: a file path said relative to the root, or a package name. */
15640
15916
  displayPath(key) {
15641
15917
  if (key.startsWith(this.root)) {
15642
- return import_node_path11.default.relative(this.root, key);
15918
+ return import_node_path12.default.relative(this.root, key);
15643
15919
  }
15644
15920
  return key;
15645
15921
  }
@@ -15710,6 +15986,17 @@ function funnelDrops(packs) {
15710
15986
  }
15711
15987
  return violations;
15712
15988
  }
15989
+ function recognizersWithNoUnits(packs) {
15990
+ return packs.filter(
15991
+ (funnel) => funnel.recognizes && !funnel.discovers && funnel.gates.length > 0 && funnel.unresolvedGates.length === 0 && funnel.candidateFiles > 0 && funnel.unitsInGatedFiles === 0 && // The closure walks a recognizer-only pack's gated exports as
15992
+ // roots, so effects recognized there mean the run worked and
15993
+ // the missing framework pack costs attribution, not existence.
15994
+ funnel.effectsRecognized === 0
15995
+ ).map((funnel) => ({
15996
+ label: funnel.pack,
15997
+ detail: `${funnel.candidateFiles} gated files, and no pack in this run discovered a unit in them. ${funnel.pack} reads calls inside units another pack finds, so add the pack that finds this project's handlers (-f express, -f fastify, ...), or run suss init to work out which.`
15998
+ }));
15999
+ }
15713
16000
  function unversionedPacks(packs) {
15714
16001
  return packs.filter((funnel) => funnel.version === null).map((funnel) => ({
15715
16002
  label: funnel.pack,
@@ -15804,6 +16091,12 @@ function evaluatePackHealth(report) {
15804
16091
  audience: "run",
15805
16092
  violations: selfCollisions(report.packs)
15806
16093
  },
16094
+ {
16095
+ name: "every recognizer had units to look inside",
16096
+ code: "no-units",
16097
+ audience: "run",
16098
+ violations: recognizersWithNoUnits(report.packs)
16099
+ },
15807
16100
  {
15808
16101
  name: "every pack declares a version",
15809
16102
  code: "no-version",