@suss/adapter-typescript 0.14.1 → 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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/adapter.ts
2
- import path9 from "path";
2
+ import path10 from "path";
3
3
  import {
4
4
  Node as Node58,
5
5
  Project,
@@ -1827,8 +1827,8 @@ function loadImportGraphsDepthFirstFromPaths(project, rootPaths) {
1827
1827
  stack.pop();
1828
1828
  loadOrder.push(top.path);
1829
1829
  let deepest = 0;
1830
- for (const path12 of top.deps) {
1831
- deepest = Math.max(deepest, chainDepth.get(path12) ?? 0);
1830
+ for (const path13 of top.deps) {
1831
+ deepest = Math.max(deepest, chainDepth.get(path13) ?? 0);
1832
1832
  }
1833
1833
  chainDepth.set(top.path, deepest + 1);
1834
1834
  }
@@ -2173,11 +2173,11 @@ function walkAndWarm(root, reach) {
2173
2173
  if (warmedFiles.get(target) !== void 0) {
2174
2174
  continue;
2175
2175
  }
2176
- const path12 = target.getFilePath();
2177
- if (visited.has(path12)) {
2176
+ const path13 = target.getFilePath();
2177
+ if (visited.has(path13)) {
2178
2178
  continue;
2179
2179
  }
2180
- visited.add(path12);
2180
+ visited.add(path13);
2181
2181
  stack.push(frameOf(target, reach));
2182
2182
  continue;
2183
2183
  }
@@ -2194,7 +2194,7 @@ function sitsOnWarmChains(file) {
2194
2194
  return false;
2195
2195
  }
2196
2196
  const imported = importedFilePathsOf(project, file.getFilePath());
2197
- return imported.length > 0 && imported.every((path12) => warmed.has(path12));
2197
+ return imported.length > 0 && imported.every((path13) => warmed.has(path13));
2198
2198
  }
2199
2199
  function markWarmed(file) {
2200
2200
  warmedFiles.set(file, true);
@@ -2651,7 +2651,7 @@ var ORIGIN = {
2651
2651
  declaredBy: (origin, receiver) => receiver.callee !== null && origin.importedFrom.some(
2652
2652
  (module) => methodDeclaredIn(receiver.callee, module, receiver.resolve)
2653
2653
  ),
2654
- constructed: (origin, receiver) => madeFromNamed(origin, madeBy(receiver)) && origin.importedFrom.some(
2654
+ constructed: (origin, receiver) => madeFromNamed(origin, madeExpression(receiver)) && origin.importedFrom.some(
2655
2655
  (module) => isImportedFrom(madeBy(receiver), module)
2656
2656
  )
2657
2657
  };
@@ -2659,14 +2659,15 @@ function madeFromNamed(origin, made) {
2659
2659
  if (origin.origin !== "constructed" || origin.named === void 0) {
2660
2660
  return true;
2661
2661
  }
2662
- return origin.named.includes(made.getText());
2662
+ return origin.named.includes(constructorName(made));
2663
+ }
2664
+ function constructorName(made) {
2665
+ const expression = Node13.isNewExpression(made) || Node13.isCallExpression(made) ? made.getExpression() : made;
2666
+ return Node13.isPropertyAccessExpression(expression) ? expression.getName() : expression.getText();
2663
2667
  }
2664
2668
  var CALL_ORIGIN = {
2665
2669
  declaredBy: ORIGIN.declaredBy,
2666
- constructed: (origin, receiver) => madeFromNamed(
2667
- origin,
2668
- rootIdentifier(receiver.expression) ?? receiver.expression
2669
- ) && origin.importedFrom.some(
2670
+ constructed: (origin, receiver) => madeFromNamed(origin, receiver.expression) && origin.importedFrom.some(
2670
2671
  (module) => isImportedFrom(
2671
2672
  rootIdentifier(receiver.expression) ?? receiver.expression,
2672
2673
  module
@@ -2674,14 +2675,19 @@ var CALL_ORIGIN = {
2674
2675
  )
2675
2676
  };
2676
2677
  function madeBy(receiver) {
2678
+ const made = madeExpression(receiver);
2679
+ const expression = Node13.isNewExpression(made) || Node13.isCallExpression(made) ? made.getExpression() : made;
2680
+ return rootIdentifier(expression) ?? expression;
2681
+ }
2682
+ function madeExpression(receiver) {
2677
2683
  if (receiver.callee === null) {
2678
- return rootIdentifier(receiver.expression) ?? receiver.expression;
2684
+ return receiver.expression;
2679
2685
  }
2680
2686
  const written = receiver.resolve(receiver.callee.getExpression());
2681
2687
  if (written === null || !Node13.isNewExpression(written) && !Node13.isCallExpression(written)) {
2682
2688
  return receiver.callee;
2683
2689
  }
2684
- return rootIdentifier(written.getExpression()) ?? receiver.callee;
2690
+ return written;
2685
2691
  }
2686
2692
  function callOpsFor(call, resolveWrittenValue) {
2687
2693
  const resolve2 = resolveWrittenValue ?? (() => null);
@@ -2722,6 +2728,7 @@ function valueOpsFor(value, resolve2) {
2722
2728
  flag: () => literalFlag(written()),
2723
2729
  entries: (unsettled) => entriesOf(written(), unsettled, resolve2),
2724
2730
  items: () => itemsOf(written(), resolve2),
2731
+ asArg: () => effectArgOf(written()),
2725
2732
  property: (name) => {
2726
2733
  const object = written();
2727
2734
  const inside = Node13.isObjectLiteralExpression(object) ? initializerOf(object, name) : null;
@@ -3209,6 +3216,9 @@ var MAX_ARG_DEPTH = 8;
3209
3216
  function extractArgs(call) {
3210
3217
  return call.getArguments().map((arg) => extractArg(arg, MAX_ARG_DEPTH));
3211
3218
  }
3219
+ function effectArgOf(node) {
3220
+ return extractArg(node, MAX_ARG_DEPTH);
3221
+ }
3212
3222
  function extractArg(node, depth) {
3213
3223
  if (Node14.isAsExpression(node) || Node14.isTypeAssertion(node) || Node14.isSatisfiesExpression(node) || Node14.isNonNullExpression(node)) {
3214
3224
  return extractArg(node.getExpression(), depth);
@@ -5808,9 +5818,9 @@ function createSourceFileLookup(project) {
5808
5818
  if (sf === null) {
5809
5819
  return null;
5810
5820
  }
5811
- const path12 = sf.getFilePath();
5812
- const index = functionsByFile.get(path12) ?? indexFunctions(sf);
5813
- functionsByFile.set(path12, index);
5821
+ const path13 = sf.getFilePath();
5822
+ const index = functionsByFile.get(path13) ?? indexFunctions(sf);
5823
+ functionsByFile.set(path13, index);
5814
5824
  return index.get(rangeKey(location.range)) ?? null;
5815
5825
  }
5816
5826
  };
@@ -6337,7 +6347,7 @@ function extractEndpointContract(endpointNode, pattern, framework) {
6337
6347
  }
6338
6348
  const responses = [];
6339
6349
  let method;
6340
- let path12;
6350
+ let path13;
6341
6351
  for (const prop of endpointNode.getProperties()) {
6342
6352
  if (!Node27.isPropertyAssignment(prop)) {
6343
6353
  continue;
@@ -6352,7 +6362,7 @@ function extractEndpointContract(endpointNode, pattern, framework) {
6352
6362
  if (propName === pattern.pathProperty) {
6353
6363
  const val = prop.getInitializer();
6354
6364
  if (val !== void 0 && Node27.isStringLiteral(val)) {
6355
- path12 = val.getLiteralValue();
6365
+ path13 = val.getLiteralValue();
6356
6366
  }
6357
6367
  }
6358
6368
  if (propName === pattern.responseExtraction.property) {
@@ -6376,10 +6386,10 @@ function extractEndpointContract(endpointNode, pattern, framework) {
6376
6386
  if (responses.length === 0) {
6377
6387
  return null;
6378
6388
  }
6379
- const boundaryBinding = method !== void 0 || path12 !== void 0 ? restBinding({
6389
+ const boundaryBinding = method !== void 0 || path13 !== void 0 ? restBinding({
6380
6390
  transport: "http",
6381
6391
  method: method ?? null,
6382
- path: path12 ?? null,
6392
+ path: path13 ?? null,
6383
6393
  recognition: framework
6384
6394
  }) : null;
6385
6395
  return {
@@ -7848,6 +7858,31 @@ function constructionRef(node, localNames, resolution) {
7848
7858
  // src/discovery/clientCall.ts
7849
7859
  import path4 from "path";
7850
7860
  import { Node as Node32 } from "ts-morph";
7861
+
7862
+ // src/discovery/resolveImport.ts
7863
+ function resolveImportedLocalName(sourceFile, importModule, importName) {
7864
+ for (const importDecl of sourceFile.getImportDeclarations()) {
7865
+ if (importDecl.getModuleSpecifierValue() !== importModule) {
7866
+ continue;
7867
+ }
7868
+ for (const named2 of importDecl.getNamedImports()) {
7869
+ if (named2.getName() === importName || named2.getAliasNode()?.getText() === importName) {
7870
+ return named2.getAliasNode()?.getText() ?? named2.getName();
7871
+ }
7872
+ }
7873
+ const defaultImport = importDecl.getDefaultImport();
7874
+ if (defaultImport !== void 0 && defaultImport.getText() === importName) {
7875
+ return defaultImport.getText();
7876
+ }
7877
+ const namespaceImport = importDecl.getNamespaceImport();
7878
+ if (namespaceImport !== void 0 && namespaceImport.getText() === importName) {
7879
+ return namespaceImport.getText();
7880
+ }
7881
+ }
7882
+ return null;
7883
+ }
7884
+
7885
+ // src/discovery/clientCall.ts
7851
7886
  function discoverClientCalls(sourceFile, match, kind, resolution) {
7852
7887
  const results = [];
7853
7888
  const isGlobal = match.importModule === "global";
@@ -7866,7 +7901,7 @@ function discoverClientCalls(sourceFile, match, kind, resolution) {
7866
7901
  if (!isGlobal && importedLocalName !== null) {
7867
7902
  for (const varDecl of sourceFile.getVariableDeclarations()) {
7868
7903
  const init = varDecl.getInitializer();
7869
- if (init !== void 0 && Node32.isCallExpression(init) && isInstanceCreationCall(init, importedLocalName, match.factoryMethods)) {
7904
+ if (init !== void 0 && (Node32.isCallExpression(init) || Node32.isNewExpression(init)) && isInstanceCreationCall(init, importedLocalName, match.factoryMethods)) {
7870
7905
  clientVarNames.add(varDecl.getName());
7871
7906
  }
7872
7907
  }
@@ -7911,12 +7946,50 @@ function discoverClientCalls(sourceFile, match, kind, resolution) {
7911
7946
  });
7912
7947
  return results;
7913
7948
  }
7949
+ function clientReceiverCheckFor(sourceFile, match, resolution) {
7950
+ const localName = resolveImportedLocalName(
7951
+ sourceFile,
7952
+ match.importModule,
7953
+ match.importName
7954
+ );
7955
+ const constructedHere = /* @__PURE__ */ new Set();
7956
+ if (localName !== null) {
7957
+ sourceFile.forEachDescendant((node) => {
7958
+ if (Node32.isVariableDeclaration(node)) {
7959
+ const init = node.getInitializer();
7960
+ if (init !== void 0 && (Node32.isCallExpression(init) || Node32.isNewExpression(init)) && isInstanceCreationCall(init, localName, match.factoryMethods) || typedAsClient(node.getTypeNode(), localName)) {
7961
+ constructedHere.add(node.getName());
7962
+ }
7963
+ return;
7964
+ }
7965
+ if (Node32.isParameterDeclaration(node) && typedAsClient(node.getTypeNode(), localName)) {
7966
+ const written = node.getNameNode();
7967
+ if (Node32.isIdentifier(written)) {
7968
+ constructedHere.add(written.getText());
7969
+ }
7970
+ }
7971
+ });
7972
+ }
7973
+ const full = {
7974
+ type: "clientCall",
7975
+ importModule: match.importModule,
7976
+ importName: match.importName,
7977
+ ...match.factoryMethods === void 0 ? {} : { factoryMethods: match.factoryMethods }
7978
+ };
7979
+ return (subject) => localName !== null && Node32.isIdentifier(subject) && subject.getText() === localName || Node32.isIdentifier(subject) && constructedHere.has(subject.getText()) || resolvesToKnownInstance(subject, full, resolution);
7980
+ }
7981
+ function typedAsClient(typeNode, localName) {
7982
+ if (typeNode === void 0 || !Node32.isTypeReference(typeNode)) {
7983
+ return false;
7984
+ }
7985
+ return typeNode.getTypeName().getText() === localName;
7986
+ }
7914
7987
  function resolvesToKnownInstance(subject, match, resolution) {
7915
7988
  if (resolution === void 0 || match.importModule === "global") {
7916
7989
  return false;
7917
7990
  }
7918
7991
  const written = writtenNodeOf(subject, resolution);
7919
- if (written === null || !Node32.isCallExpression(written)) {
7992
+ if (written === null || !Node32.isCallExpression(written) && !Node32.isNewExpression(written)) {
7920
7993
  return false;
7921
7994
  }
7922
7995
  const writtenLocalName = resolvedImportLocalName(
@@ -8238,7 +8311,34 @@ function joinRoutePath(prefix, suffix) {
8238
8311
  }
8239
8312
  return `/${segments.join("/")}`;
8240
8313
  }
8241
- function discoverDecoratedRoutes(sourceFile, match, kind, resolution) {
8314
+ function declaredChannelOf(source, decorator, resolution) {
8315
+ if (source.from === "literal") {
8316
+ return source.value;
8317
+ }
8318
+ if (source.from === "unstated") {
8319
+ return null;
8320
+ }
8321
+ if (!Node36.isDecorator(decorator)) {
8322
+ return null;
8323
+ }
8324
+ const arg = decorator.getArguments()[source.position];
8325
+ if (arg === void 0) {
8326
+ return null;
8327
+ }
8328
+ const text = literalStringOf(arg, resolution);
8329
+ return text === null || text === "" ? null : text;
8330
+ }
8331
+ function literalStringOf(node, resolution) {
8332
+ if (Node36.isStringLiteral(node) || Node36.isNoSubstitutionTemplateLiteral(node)) {
8333
+ return node.getLiteralValue();
8334
+ }
8335
+ const written = resolution?.resolveWrittenValue(node) ?? null;
8336
+ if (written === null || written === node) {
8337
+ return null;
8338
+ }
8339
+ return literalStringOf(written, resolution);
8340
+ }
8341
+ function discoverDecoratedRoutes(sourceFile, match, kind, resolution, binding) {
8242
8342
  const acceptedModules = Array.isArray(match.importModule) ? match.importModule : [match.importModule];
8243
8343
  const routeDecoratorNames = Object.keys(match.methodDecoratorRouteMap);
8244
8344
  const importedRouteDecorators = /* @__PURE__ */ new Set();
@@ -8270,6 +8370,22 @@ function discoverDecoratedRoutes(sourceFile, match, kind, resolution) {
8270
8370
  const pathPrefix = routePathOf(marker.args, resolution);
8271
8371
  const className = cls.getName() ?? "<anon-class>";
8272
8372
  for (const handler of decoratedCallablesOf(cls, routeDecoratorNames)) {
8373
+ if (binding !== void 0) {
8374
+ results.push({
8375
+ func: handler.func,
8376
+ kind,
8377
+ name: `${className}.${handler.name}`,
8378
+ channelInfo: {
8379
+ messageBus: binding.messageBus,
8380
+ channel: declaredChannelOf(
8381
+ binding.channel,
8382
+ handler.decorator,
8383
+ resolution
8384
+ )
8385
+ }
8386
+ });
8387
+ continue;
8388
+ }
8273
8389
  const httpMethod = match.methodDecoratorRouteMap[handler.standsFor];
8274
8390
  const pathSuffix = resolveRoutePathArg(handler.decorator, resolution);
8275
8391
  const routePath = joinRoutePath(pathPrefix, pathSuffix);
@@ -9117,40 +9233,8 @@ function discoverGraphqlHookCalls(sourceFile, match, kind, resolution) {
9117
9233
 
9118
9234
  // src/discovery/graphqlImperativeCall.ts
9119
9235
  import { Node as Node40 } from "ts-morph";
9120
-
9121
- // src/discovery/resolveImport.ts
9122
- function resolveImportedLocalName(sourceFile, importModule, importName) {
9123
- for (const importDecl of sourceFile.getImportDeclarations()) {
9124
- if (importDecl.getModuleSpecifierValue() !== importModule) {
9125
- continue;
9126
- }
9127
- for (const named2 of importDecl.getNamedImports()) {
9128
- if (named2.getName() === importName || named2.getAliasNode()?.getText() === importName) {
9129
- return named2.getAliasNode()?.getText() ?? named2.getName();
9130
- }
9131
- }
9132
- const defaultImport = importDecl.getDefaultImport();
9133
- if (defaultImport !== void 0 && defaultImport.getText() === importName) {
9134
- return defaultImport.getText();
9135
- }
9136
- const namespaceImport = importDecl.getNamespaceImport();
9137
- if (namespaceImport !== void 0 && namespaceImport.getText() === importName) {
9138
- return namespaceImport.getText();
9139
- }
9140
- }
9141
- return null;
9142
- }
9143
-
9144
- // src/discovery/graphqlImperativeCall.ts
9145
9236
  function discoverGraphqlImperativeCalls(sourceFile, match, kind, resolution) {
9146
- const localName = resolveImportedLocalName(
9147
- sourceFile,
9148
- match.importModule,
9149
- match.importName
9150
- );
9151
- if (localName === null) {
9152
- return [];
9153
- }
9237
+ const isTheClient = clientReceiverCheckFor(sourceFile, match, resolution);
9154
9238
  const methodSpec = /* @__PURE__ */ new Map();
9155
9239
  for (const method of match.methods) {
9156
9240
  methodSpec.set(method.methodName, {
@@ -9172,6 +9256,9 @@ function discoverGraphqlImperativeCalls(sourceFile, match, kind, resolution) {
9172
9256
  if (spec === void 0) {
9173
9257
  return;
9174
9258
  }
9259
+ if (!isTheClient(callee.getExpression())) {
9260
+ return;
9261
+ }
9175
9262
  const args = node.getArguments();
9176
9263
  if (args.length === 0 || !Node40.isObjectLiteralExpression(args[0])) {
9177
9264
  return;
@@ -9318,7 +9405,7 @@ function attributeValueOf(node, name) {
9318
9405
  }
9319
9406
  return null;
9320
9407
  }
9321
- function literalStringOf(value) {
9408
+ function literalStringOf2(value) {
9322
9409
  const unwrapped2 = Node41.isJsxExpression(value) ? value.getExpression() ?? value : value;
9323
9410
  if (Node41.isStringLiteral(unwrapped2) || Node41.isNoSubstitutionTemplateLiteral(unwrapped2)) {
9324
9411
  return unwrapped2.getLiteralValue();
@@ -9328,7 +9415,7 @@ function literalStringOf(value) {
9328
9415
  function ownPathOfJsx(node, match) {
9329
9416
  const pathValue = attributeValueOf(node, match.pathAttribute);
9330
9417
  if (pathValue !== null) {
9331
- const literal = literalStringOf(pathValue);
9418
+ const literal = literalStringOf2(pathValue);
9332
9419
  return literal !== null ? { kind: "literal", value: literal } : { kind: "unreadable" };
9333
9420
  }
9334
9421
  if (match.indexAttribute !== void 0 && attributeValueOf(node, match.indexAttribute) !== null) {
@@ -9419,14 +9506,14 @@ function unitForJsxRoute(node, match, routeNames, kind, resolution) {
9419
9506
  }
9420
9507
  return routeUnit(node, kind, match.method, resolved.path, target, resolution);
9421
9508
  }
9422
- function routeUnit(at, kind, method, path12, target, resolution) {
9423
- const routeInfo = { method, path: path12 };
9509
+ function routeUnit(at, kind, method, path13, target, resolution) {
9510
+ const routeInfo = { method, path: path13 };
9424
9511
  if (target.kind === "unreadable") {
9425
9512
  return {
9426
9513
  func: null,
9427
9514
  announcedAt: at,
9428
9515
  kind,
9429
- name: path12,
9516
+ name: path13,
9430
9517
  nameKind: "label",
9431
9518
  routeInfo
9432
9519
  };
@@ -9596,7 +9683,7 @@ function unitsUnderRouteObject(object, match, kind, resolution, prefix, ancestry
9596
9683
  function ownPathOfObject(object, match) {
9597
9684
  const pathValue = routePropertyOf(object, match.pathAttribute);
9598
9685
  if (pathValue !== null) {
9599
- const literal = literalStringOf(pathValue);
9686
+ const literal = literalStringOf2(pathValue);
9600
9687
  return literal !== null ? { kind: "literal", value: literal } : { kind: "unreadable" };
9601
9688
  }
9602
9689
  if (match.indexAttribute !== void 0) {
@@ -9856,6 +9943,9 @@ function resolvePackageExportsCached(packageJsonPath) {
9856
9943
  return fresh;
9857
9944
  }
9858
9945
  function discoverPackageExports(sourceFile, match, kind) {
9946
+ if (match.packageJsonPath === void 0) {
9947
+ return [];
9948
+ }
9859
9949
  const { entries } = resolvePackageExportsCached(match.packageJsonPath);
9860
9950
  const filePath = sourceFile.getFilePath();
9861
9951
  const matching = [];
@@ -10243,12 +10333,12 @@ function originEndOf(flattened) {
10243
10333
  const slash = flattened.indexOf("/", opener[0].length);
10244
10334
  return slash === -1 ? flattened.length : slash;
10245
10335
  }
10246
- function appendPathText(path12, text) {
10336
+ function appendPathText(path13, text) {
10247
10337
  const idx = text.search(/[?#]/);
10248
10338
  if (idx === -1) {
10249
- return { path: path12 + text, stop: false };
10339
+ return { path: path13 + text, stop: false };
10250
10340
  }
10251
- return { path: path12 + text.slice(0, idx), stop: true };
10341
+ return { path: path13 + text.slice(0, idx), stop: true };
10252
10342
  }
10253
10343
  function parseAbsoluteUrl(text) {
10254
10344
  try {
@@ -10272,8 +10362,8 @@ function pathnameOfAbsoluteLiteral(text) {
10272
10362
  return stripQueryAndFragment(stripOriginManually(text));
10273
10363
  }
10274
10364
  function pathFromLiteralUrl(text) {
10275
- const path12 = isAbsoluteUrlLiteral(text) ? pathnameOfAbsoluteLiteral(text) : stripQueryAndFragment(text);
10276
- return path12 === "" ? void 0 : path12;
10365
+ const path13 = isAbsoluteUrlLiteral(text) ? pathnameOfAbsoluteLiteral(text) : stripQueryAndFragment(text);
10366
+ return path13 === "" ? void 0 : path13;
10277
10367
  }
10278
10368
  function pathFromTemplateUrl(arg, resolution) {
10279
10369
  const headText = arg.getHead().getLiteralText();
@@ -10281,7 +10371,7 @@ function pathFromTemplateUrl(arg, resolution) {
10281
10371
  const tails = spans.map((span) => span.getLiteral().getLiteralText());
10282
10372
  const originEnd = originEndOf([headText, ...tails].join(SUBSTITUTION));
10283
10373
  const head = appendPathText("", headText.slice(originEnd));
10284
- let path12 = head.path;
10374
+ let path13 = head.path;
10285
10375
  let stop = head.stop;
10286
10376
  let at = headText.length;
10287
10377
  for (let i = 0; i < spans.length && !stop; i++) {
@@ -10291,18 +10381,18 @@ function pathFromTemplateUrl(arg, resolution) {
10291
10381
  continue;
10292
10382
  }
10293
10383
  if (at >= originEnd) {
10294
- path12 += holeText(span.getExpression(), resolution);
10384
+ path13 += holeText(span.getExpression(), resolution);
10295
10385
  }
10296
10386
  at += SUBSTITUTION.length;
10297
10387
  const appended = appendPathText(
10298
- path12,
10388
+ path13,
10299
10389
  tailText.slice(Math.max(0, originEnd - at))
10300
10390
  );
10301
- path12 = appended.path;
10391
+ path13 = appended.path;
10302
10392
  stop = appended.stop;
10303
10393
  at += tailText.length;
10304
10394
  }
10305
- return path12 === "" ? void 0 : path12;
10395
+ return path13 === "" ? void 0 : path13;
10306
10396
  }
10307
10397
  function pathFromUrlNode(node, resolution) {
10308
10398
  if (Node46.isStringLiteral(node)) {
@@ -10314,8 +10404,24 @@ function pathFromUrlNode(node, resolution) {
10314
10404
  if (Node46.isTemplateExpression(node)) {
10315
10405
  return pathFromTemplateUrl(node, resolution);
10316
10406
  }
10407
+ if (Node46.isBinaryExpression(node)) {
10408
+ const joined = joinedStringOf(node, resolution);
10409
+ return joined === void 0 ? void 0 : pathFromLiteralUrl(joined);
10410
+ }
10317
10411
  return void 0;
10318
10412
  }
10413
+ function joinedStringOf(node, resolution) {
10414
+ if (Node46.isStringLiteral(node) || Node46.isNoSubstitutionTemplateLiteral(node)) {
10415
+ return node.getLiteralValue();
10416
+ }
10417
+ if (Node46.isBinaryExpression(node) && node.getOperatorToken().getText() === "+") {
10418
+ const left = joinedStringOf(node.getLeft(), resolution);
10419
+ const right = joinedStringOf(node.getRight(), resolution);
10420
+ return left === void 0 || right === void 0 ? void 0 : left + right;
10421
+ }
10422
+ const written = resolution?.resolveWrittenValue(node) ?? null;
10423
+ return written === null || written === node ? void 0 : joinedStringOf(written, resolution);
10424
+ }
10319
10425
  function placeholderName(expr) {
10320
10426
  if (Node46.isIdentifier(expr)) {
10321
10427
  return expr.getText();
@@ -10453,9 +10559,9 @@ function withMountPrefix(routeInfo, subjectNode, mountPrefixes) {
10453
10559
  const prefix = mountPrefixes.effectivePrefixFor(subjectNode);
10454
10560
  return prefix === "" ? routeInfo : { ...routeInfo, path: joinMountedPath(prefix, routeInfo.path) };
10455
10561
  }
10456
- function joinMountedPath(prefix, path12) {
10562
+ function joinMountedPath(prefix, path13) {
10457
10563
  const trimmed = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
10458
- return trimmed === "" ? path12 : trimmed + path12;
10564
+ return trimmed === "" ? path13 : trimmed + path13;
10459
10565
  }
10460
10566
  function registrationSubjectsOf(sourceFile, importModule, importName) {
10461
10567
  let importedLocalName = null;
@@ -10619,11 +10725,11 @@ function extractRouteInfoFromBinding(call, methodName, binding, resolution) {
10619
10725
  return null;
10620
10726
  }
10621
10727
  const method2 = stringProperty(routeObject, binding.method.property);
10622
- const path13 = stringProperty(routeObject, binding.path.property);
10623
- if (method2 === null || path13 === null) {
10728
+ const path14 = stringProperty(routeObject, binding.path.property);
10729
+ if (method2 === null || path14 === null) {
10624
10730
  return null;
10625
10731
  }
10626
- return { method: method2.toUpperCase(), path: path13 };
10732
+ return { method: method2.toUpperCase(), path: path14 };
10627
10733
  }
10628
10734
  if (binding.method.type !== "fromRegistration" || binding.path.type !== "fromArgument") {
10629
10735
  return null;
@@ -10640,26 +10746,46 @@ function extractRouteInfoFromBinding(call, methodName, binding, resolution) {
10640
10746
  method = arg.getLiteralValue().toUpperCase();
10641
10747
  }
10642
10748
  const pathArg = call.getArguments()[binding.path.position];
10643
- const path12 = pathArg === void 0 ? void 0 : pathFromArgument(pathArg, resolution);
10644
- return path12 === void 0 ? null : { method, path: path12 };
10749
+ const path13 = pathArg === void 0 ? void 0 : pathFromArgument(pathArg, resolution);
10750
+ return path13 === void 0 ? null : { method, path: path13 };
10645
10751
  }
10646
10752
 
10647
10753
  // src/discovery/registrationLoop.ts
10648
10754
  import { Node as Node48 } from "ts-morph";
10649
10755
  function discoverRegistrationLoops(sourceFile, match, kind, resolution) {
10650
10756
  const results = [];
10757
+ const routables = routableNames(sourceFile, match);
10758
+ if (routables !== null && routables.size === 0) {
10759
+ return results;
10760
+ }
10651
10761
  sourceFile.forEachDescendant((node) => {
10652
10762
  if (!Node48.isForOfStatement(node)) {
10653
10763
  return;
10654
10764
  }
10655
- const expanded = tryExpandLoop(node, match, kind, resolution);
10765
+ const expanded = tryExpandLoop(node, match, kind, resolution, routables);
10656
10766
  if (expanded !== null) {
10657
10767
  results.push(...expanded);
10658
10768
  }
10659
10769
  });
10660
10770
  return results;
10661
10771
  }
10662
- function tryExpandLoop(loop, match, kind, resolution) {
10772
+ function routableNames(sourceFile, match) {
10773
+ if (match.receiver === void 0) {
10774
+ return null;
10775
+ }
10776
+ const names = /* @__PURE__ */ new Set();
10777
+ for (const importName of match.receiver.importNames) {
10778
+ for (const name of registrationSubjectsOf(
10779
+ sourceFile,
10780
+ match.receiver.importModule,
10781
+ importName
10782
+ ).keys()) {
10783
+ names.add(name);
10784
+ }
10785
+ }
10786
+ return names;
10787
+ }
10788
+ function tryExpandLoop(loop, match, kind, resolution, routables) {
10663
10789
  const loopVar = loopVariableName(loop);
10664
10790
  if (loopVar === null) {
10665
10791
  return null;
@@ -10667,6 +10793,9 @@ function tryExpandLoop(loop, match, kind, resolution) {
10667
10793
  if (!bodyReferencesLoopVar(loop, loopVar)) {
10668
10794
  return null;
10669
10795
  }
10796
+ if (routables !== null && !bodyCallsOn(loop, routables)) {
10797
+ return null;
10798
+ }
10670
10799
  const arrayLit = arrayLiteralOf(loop.getExpression(), resolution);
10671
10800
  if (arrayLit === null) {
10672
10801
  return null;
@@ -10725,9 +10854,23 @@ function bodyReferencesLoopVar(loop, name) {
10725
10854
  });
10726
10855
  return referenced;
10727
10856
  }
10857
+ function bodyCallsOn(loop, routables) {
10858
+ let found = false;
10859
+ loop.getStatement().forEachDescendant((node) => {
10860
+ if (found || !Node48.isCallExpression(node)) {
10861
+ return;
10862
+ }
10863
+ const callee = node.getExpression();
10864
+ const subject = Node48.isPropertyAccessExpression(callee) || Node48.isElementAccessExpression(callee) ? callee.getExpression() : null;
10865
+ if (subject !== null && Node48.isIdentifier(subject) && routables.has(subject.getText())) {
10866
+ found = true;
10867
+ }
10868
+ });
10869
+ return found;
10870
+ }
10728
10871
  function readRouteSpec(element, shape, resolution) {
10729
10872
  let method = null;
10730
- let path12 = null;
10873
+ let path13 = null;
10731
10874
  let handler = null;
10732
10875
  for (const prop of element.getProperties()) {
10733
10876
  if (Node48.isMethodDeclaration(prop) && prop.getName() === shape.handlerKey) {
@@ -10745,16 +10888,16 @@ function readRouteSpec(element, shape, resolution) {
10745
10888
  if (name === shape.methodKey) {
10746
10889
  method = readStringLiteralValue(init);
10747
10890
  } else if (name === shape.pathKey) {
10748
- path12 = readStringLiteralValue(init);
10891
+ path13 = readStringLiteralValue(init);
10749
10892
  } else if (name === shape.handlerKey) {
10750
10893
  const func = functionValueOf(init, resolution);
10751
10894
  handler = func === null ? null : { func, name: handlerName(init) };
10752
10895
  }
10753
10896
  }
10754
- if (method === null || path12 === null || handler === null) {
10897
+ if (method === null || path13 === null || handler === null) {
10755
10898
  return null;
10756
10899
  }
10757
- return { method, path: path12, handler: handler.func, handlerName: handler.name };
10900
+ return { method, path: path13, handler: handler.func, handlerName: handler.name };
10758
10901
  }
10759
10902
  function readStringLiteralValue(node) {
10760
10903
  if (Node48.isStringLiteral(node) || Node48.isNoSubstitutionTemplateLiteral(node)) {
@@ -10783,8 +10926,8 @@ function discoverRegistrationTemplates(sourceFile, match, kind, resolution) {
10783
10926
  }
10784
10927
  const args = node.getArguments();
10785
10928
  for (const reg of match.registrations) {
10786
- const path12 = substitutePath(reg.pathTemplate, args);
10787
- if (path12 === null) {
10929
+ const path13 = substitutePath(reg.pathTemplate, args);
10930
+ if (path13 === null) {
10788
10931
  continue;
10789
10932
  }
10790
10933
  const handler = resolveHandler(reg.handlerArg, args, resolution);
@@ -10797,7 +10940,7 @@ function discoverRegistrationTemplates(sourceFile, match, kind, resolution) {
10797
10940
  name: handler.name,
10798
10941
  routeInfo: {
10799
10942
  method: reg.method.toUpperCase(),
10800
- path: path12
10943
+ path: path13
10801
10944
  }
10802
10945
  });
10803
10946
  }
@@ -11123,7 +11266,8 @@ function runPattern(sourceFile, pattern, resolution, mountPrefixes) {
11123
11266
  sourceFile,
11124
11267
  pattern.match,
11125
11268
  pattern.kind,
11126
- resolution
11269
+ resolution,
11270
+ pattern.binding
11127
11271
  );
11128
11272
  }
11129
11273
  if (pattern.match.type === "jsxElementRoute") {
@@ -12411,7 +12555,38 @@ var REACHABLE_RULES = [
12411
12555
  )
12412
12556
  ];
12413
12557
  var NO_RECOGNIZERS = { invocation: [], access: [] };
12414
- function expandReachableClosure(seeds, project, options, projectFileSet, facts, recognizers = NO_RECOGNIZERS, alreadySummarized = []) {
12558
+ function recognizerOnlyRoots(packsByFile) {
12559
+ const roots = [];
12560
+ for (const [sourceFile, packs] of packsByFile) {
12561
+ const recognizerOnly = packs.some(
12562
+ (pack) => pack.discovery.length === 0 && pack.discoverUnits === void 0 && ((pack.invocationRecognizers?.length ?? 0) > 0 || (pack.accessRecognizers?.length ?? 0) > 0)
12563
+ );
12564
+ if (!recognizerOnly) {
12565
+ continue;
12566
+ }
12567
+ for (const declaration of sourceFile.getFunctions()) {
12568
+ if (declaration.isExported() && hasBody(declaration)) {
12569
+ roots.push({
12570
+ func: declaration,
12571
+ name: declaration.getName() ?? "<anon>"
12572
+ });
12573
+ }
12574
+ }
12575
+ for (const variableStatement of sourceFile.getVariableStatements()) {
12576
+ if (!variableStatement.isExported()) {
12577
+ continue;
12578
+ }
12579
+ for (const declaration of variableStatement.getDeclarations()) {
12580
+ const init = declaration.getInitializer();
12581
+ if (init !== void 0 && (Node54.isArrowFunction(init) || Node54.isFunctionExpression(init)) && hasBody(init)) {
12582
+ roots.push({ func: init, name: declaration.getName() });
12583
+ }
12584
+ }
12585
+ }
12586
+ }
12587
+ return roots;
12588
+ }
12589
+ function expandReachableClosure(seeds, project, options, projectFileSet, facts, recognizers = NO_RECOGNIZERS, alreadySummarized = [], extraRoots = []) {
12415
12590
  const lookup = createSourceFileLookup(project);
12416
12591
  const db = facts?.db ?? new Database3();
12417
12592
  const functionByKey = /* @__PURE__ */ new Map();
@@ -12431,6 +12606,13 @@ function expandReachableClosure(seeds, project, options, projectFileSet, facts,
12431
12606
  db.add("entry", [key]);
12432
12607
  }
12433
12608
  }
12609
+ for (const root of extraRoots) {
12610
+ const key = nodeKey2(root.func);
12611
+ if (!functionByKey.has(key)) {
12612
+ functionByKey.set(key, root);
12613
+ }
12614
+ db.add("entry", [key]);
12615
+ }
12434
12616
  const knownKeys = /* @__PURE__ */ new Set();
12435
12617
  for (const summary of alreadySummarized) {
12436
12618
  const func = lookup.functionAt(summary.location);
@@ -13498,6 +13680,185 @@ function computeAdapterPacksDigest(packVersions) {
13498
13680
  return `${adapterStamp}|${sortedPacks.join(",")}`;
13499
13681
  }
13500
13682
 
13683
+ // src/workspacePatterns.ts
13684
+ import { createHash as createHash4 } from "crypto";
13685
+ import fs8 from "fs";
13686
+ import path9 from "path";
13687
+ function readJson(filePath) {
13688
+ try {
13689
+ return JSON.parse(fs8.readFileSync(filePath, "utf8"));
13690
+ } catch {
13691
+ return void 0;
13692
+ }
13693
+ }
13694
+ function pnpmWorkspaceGlobs(yamlText) {
13695
+ const globs = [];
13696
+ let inPackages = false;
13697
+ for (const line of yamlText.split("\n")) {
13698
+ if (/^packages\s*:/.test(line)) {
13699
+ inPackages = true;
13700
+ continue;
13701
+ }
13702
+ if (inPackages) {
13703
+ const item = line.match(/^\s+-\s+["']?([^"'#\s]+)["']?/);
13704
+ if (item !== null) {
13705
+ globs.push(item[1]);
13706
+ continue;
13707
+ }
13708
+ if (/^\S/.test(line)) {
13709
+ inPackages = false;
13710
+ }
13711
+ }
13712
+ }
13713
+ return globs;
13714
+ }
13715
+ function workspaceGlobsAt(dir) {
13716
+ const pkg = readJson(path9.join(dir, "package.json"));
13717
+ if (typeof pkg === "object" && pkg !== null && "workspaces" in pkg) {
13718
+ const ws = pkg.workspaces;
13719
+ if (Array.isArray(ws)) {
13720
+ return ws.filter((g) => typeof g === "string");
13721
+ }
13722
+ if (typeof ws === "object" && ws !== null && Array.isArray(ws.packages)) {
13723
+ return ws.packages.filter(
13724
+ (g) => typeof g === "string"
13725
+ );
13726
+ }
13727
+ }
13728
+ const pnpmPath = path9.join(dir, "pnpm-workspace.yaml");
13729
+ if (fs8.existsSync(pnpmPath)) {
13730
+ try {
13731
+ return pnpmWorkspaceGlobs(fs8.readFileSync(pnpmPath, "utf8"));
13732
+ } catch {
13733
+ return null;
13734
+ }
13735
+ }
13736
+ return null;
13737
+ }
13738
+ function isDirectory(candidate) {
13739
+ try {
13740
+ return fs8.statSync(candidate).isDirectory();
13741
+ } catch {
13742
+ return false;
13743
+ }
13744
+ }
13745
+ function subdirectoriesOf(dir) {
13746
+ try {
13747
+ return fs8.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).filter((name) => !name.startsWith(".") && name !== "node_modules");
13748
+ } catch {
13749
+ return [];
13750
+ }
13751
+ }
13752
+ function segmentToRegExp(segment) {
13753
+ const escaped = segment.replace(/[.+^${}()|[\]\\]/g, "\\$&");
13754
+ return new RegExp(`^${escaped.replace(/\*/g, "[^/]*")}$`);
13755
+ }
13756
+ function directoriesMatchingGlob(root, glob) {
13757
+ let current2 = [root];
13758
+ for (const segment of glob.split("/").filter((s) => s.length > 0)) {
13759
+ const next = [];
13760
+ if (segment.includes("*")) {
13761
+ const pattern = segmentToRegExp(segment);
13762
+ for (const dir of current2) {
13763
+ for (const entry of subdirectoriesOf(dir)) {
13764
+ if (pattern.test(entry)) {
13765
+ next.push(path9.join(dir, entry));
13766
+ }
13767
+ }
13768
+ }
13769
+ } else {
13770
+ for (const dir of current2) {
13771
+ next.push(path9.join(dir, segment));
13772
+ }
13773
+ }
13774
+ current2 = next.filter(isDirectory);
13775
+ }
13776
+ return current2;
13777
+ }
13778
+ function packageAt(dir) {
13779
+ const packageJsonPath = path9.join(dir, "package.json");
13780
+ const pkg = readJson(packageJsonPath);
13781
+ if (typeof pkg === "object" && pkg !== null && typeof pkg.name === "string") {
13782
+ return { name: pkg.name, packageJsonPath };
13783
+ }
13784
+ return null;
13785
+ }
13786
+ function workspacePackagesFrom(anchor) {
13787
+ let dir = path9.resolve(anchor);
13788
+ for (; ; ) {
13789
+ const globs = workspaceGlobsAt(dir);
13790
+ if (globs !== null) {
13791
+ const found = /* @__PURE__ */ new Map();
13792
+ for (const glob of globs) {
13793
+ if (glob.startsWith("!")) {
13794
+ continue;
13795
+ }
13796
+ for (const match of directoriesMatchingGlob(dir, glob)) {
13797
+ const pkg = packageAt(match);
13798
+ if (pkg !== null) {
13799
+ found.set(pkg.packageJsonPath, pkg);
13800
+ }
13801
+ }
13802
+ }
13803
+ return [...found.values()].sort(
13804
+ (a, b) => a.packageJsonPath.localeCompare(b.packageJsonPath)
13805
+ );
13806
+ }
13807
+ const parent = path9.dirname(dir);
13808
+ if (parent === dir) {
13809
+ return [];
13810
+ }
13811
+ dir = parent;
13812
+ }
13813
+ }
13814
+ function isWorkspaceMarked(pattern) {
13815
+ const match = pattern.match;
13816
+ return (match.type === "packageExports" || match.type === "packageImport") && match.workspaces === true;
13817
+ }
13818
+ function expandPattern(pattern, packages) {
13819
+ const match = pattern.match;
13820
+ if (match.type === "packageExports" && match.workspaces === true) {
13821
+ const { workspaces: _workspaces, ...rest } = match;
13822
+ return packages.map((pkg) => ({
13823
+ ...pattern,
13824
+ match: { ...rest, packageJsonPath: pkg.packageJsonPath }
13825
+ }));
13826
+ }
13827
+ if (match.type === "packageImport" && match.workspaces === true) {
13828
+ const listed = match.packages ?? [];
13829
+ const names = [.../* @__PURE__ */ new Set([...listed, ...packages.map((p) => p.name)])];
13830
+ if (names.length === 0) {
13831
+ return [];
13832
+ }
13833
+ return [{ ...pattern, match: { type: "packageImport", packages: names } }];
13834
+ }
13835
+ return [pattern];
13836
+ }
13837
+ function workspaceExpansionStamp(frameworks) {
13838
+ const paths = frameworks.flatMap((pack) => pack.discovery).flatMap(
13839
+ (pattern) => pattern.match.type === "packageExports" && pattern.match.packageJsonPath !== void 0 ? [pattern.match.packageJsonPath] : []
13840
+ ).sort();
13841
+ if (paths.length === 0) {
13842
+ return "none";
13843
+ }
13844
+ return createHash4("sha256").update(paths.join("\n")).digest("hex").slice(0, 12);
13845
+ }
13846
+ function expandWorkspacePatterns(frameworks, anchor) {
13847
+ const marked = frameworks.some(
13848
+ (pack) => pack.discovery.some(isWorkspaceMarked)
13849
+ );
13850
+ if (!marked) {
13851
+ return frameworks;
13852
+ }
13853
+ const packages = anchor !== void 0 ? workspacePackagesFrom(anchor) : [];
13854
+ return frameworks.map((pack) => ({
13855
+ ...pack,
13856
+ discovery: pack.discovery.flatMap(
13857
+ (pattern) => expandPattern(pattern, packages)
13858
+ )
13859
+ }));
13860
+ }
13861
+
13501
13862
  // src/adapter.ts
13502
13863
  var raise = (msg) => {
13503
13864
  throw new Error(msg);
@@ -13908,11 +14269,11 @@ function extractConsumerBinding(unit, pattern, pack, resolution) {
13908
14269
  return null;
13909
14270
  }
13910
14271
  const method = extractBindingMethod(binding, callSite, pack, resolution);
13911
- const path12 = extractBindingPath(binding, callSite, pack, resolution);
14272
+ const path13 = extractBindingPath(binding, callSite, pack, resolution);
13912
14273
  return restBinding2({
13913
14274
  transport: pack.protocol,
13914
14275
  method: method ?? null,
13915
- path: path12 ?? null,
14276
+ path: path13 ?? null,
13916
14277
  recognition: pack.name
13917
14278
  });
13918
14279
  }
@@ -14524,8 +14885,8 @@ function synthesizeCallerSummaries(wrapper, _project, options, resolution) {
14524
14885
  if (pathArg === void 0) {
14525
14886
  continue;
14526
14887
  }
14527
- const path12 = pathFromArgument(pathArg, resolution);
14528
- if (path12 === void 0) {
14888
+ const path13 = pathFromArgument(pathArg, resolution);
14889
+ if (path13 === void 0) {
14529
14890
  continue;
14530
14891
  }
14531
14892
  const callerFunc = enclosingFunction(callExpr);
@@ -14537,7 +14898,7 @@ function synthesizeCallerSummaries(wrapper, _project, options, resolution) {
14537
14898
  continue;
14538
14899
  }
14539
14900
  seen.add(dedupKey);
14540
- out.push(buildCallerSummary(wrapper, callerFunc, callExpr, path12, options));
14901
+ out.push(buildCallerSummary(wrapper, callerFunc, callExpr, path13, options));
14541
14902
  }
14542
14903
  return out;
14543
14904
  }
@@ -14594,7 +14955,7 @@ function callerName(func) {
14594
14955
  }
14595
14956
  return "anonymous";
14596
14957
  }
14597
- function buildCallerSummary(wrapper, callerFunc, callExpr, path12, options) {
14958
+ function buildCallerSummary(wrapper, callerFunc, callExpr, path13, options) {
14598
14959
  const wrapperBinding = wrapper.summary.identity.boundaryBinding;
14599
14960
  const wrapperRest = wrapperBinding?.semantics.name === "rest" ? wrapperBinding.semantics : null;
14600
14961
  const syntheticPack = {
@@ -14626,7 +14987,7 @@ function buildCallerSummary(wrapper, callerFunc, callExpr, path12, options) {
14626
14987
  raw.boundaryBinding = restBinding2({
14627
14988
  transport: wrapperBinding?.transport ?? "http",
14628
14989
  method: wrapperRest?.method ?? null,
14629
- path: path12,
14990
+ path: path13,
14630
14991
  recognition: wrapperBinding?.recognition ?? "unknown"
14631
14992
  });
14632
14993
  const summary = assembleSummary3(raw, options);
@@ -14692,25 +15053,32 @@ function loadRunFiles(project, candidatePaths, timer) {
14692
15053
  );
14693
15054
  return { sourceFiles, deep };
14694
15055
  }
14695
- function createTypeScriptAdapter(config) {
15056
+ function createTypeScriptAdapter(suppliedConfig) {
15057
+ const config = {
15058
+ ...suppliedConfig,
15059
+ frameworks: expandWorkspacePatterns(
15060
+ suppliedConfig.frameworks,
15061
+ suppliedConfig.projectRoot ?? (suppliedConfig.tsConfigFilePath !== void 0 ? path10.dirname(suppliedConfig.tsConfigFilePath) : void 0)
15062
+ )
15063
+ };
14696
15064
  const project = config.project ?? new Project(
14697
15065
  config.tsConfigFilePath !== void 0 ? {
14698
15066
  tsConfigFilePath: config.tsConfigFilePath,
14699
15067
  skipAddingFilesFromTsConfig: true
14700
15068
  } : { skipAddingFilesFromTsConfig: true }
14701
15069
  );
14702
- const runRoot = config.projectRoot ?? (config.tsConfigFilePath !== void 0 ? workspaceRootFor(path9.dirname(config.tsConfigFilePath)) : void 0);
15070
+ const runRoot = config.projectRoot ?? (config.tsConfigFilePath !== void 0 ? workspaceRootFor(path10.dirname(config.tsConfigFilePath)) : void 0);
14703
15071
  let lazyBootstrapped = false;
14704
15072
  let projectFileSet;
14705
15073
  const cacheDir = declineWhenRunFromSource(
14706
- config.cacheDir === null ? null : config.cacheDir ?? (config.tsConfigFilePath !== void 0 ? path9.join(path9.dirname(config.tsConfigFilePath), ".suss", "cache") : null)
15074
+ config.cacheDir === null ? null : config.cacheDir ?? (config.tsConfigFilePath !== void 0 ? path10.join(path10.dirname(config.tsConfigFilePath), ".suss", "cache") : null)
14707
15075
  );
14708
15076
  const cache = createCacheLayer(cacheDir);
14709
15077
  const adapterPacksDigest = `${computeAdapterPacksDigest(
14710
15078
  config.frameworks.map(
14711
15079
  (p) => p.version !== void 0 ? { name: p.name, version: p.version } : { name: p.name }
14712
15080
  )
14713
- )}|${extractionConfigStamp(config)}`;
15081
+ )}|${extractionConfigStamp(config)}|ws:${workspaceExpansionStamp(config.frameworks)}`;
14714
15082
  const packWrappers = config.frameworks.flatMap(
14715
15083
  (pack) => pack.transparentWrappers ?? []
14716
15084
  );
@@ -14946,7 +15314,12 @@ function createTypeScriptAdapter(config) {
14946
15314
  },
14947
15315
  // Reached units the cache already serves emit nothing,
14948
15316
  // the way a cold run's seeds do not.
14949
- reused?.summaries ?? []
15317
+ reused?.summaries ?? [],
15318
+ // A recognizer-only pack's effects need a function to
15319
+ // live on even when nothing discovers units in its
15320
+ // files, so those files' exports join the walk as
15321
+ // roots.
15322
+ recognizerOnlyRoots(packsByFile)
14950
15323
  )
14951
15324
  ) : withSubUnits;
14952
15325
  const closureOwnership = caching && config.includeReachable !== false ? timer.time(
@@ -15343,8 +15716,8 @@ function buildSubUnitSummary(subUnit, parent, invocationRecognizers, accessRecog
15343
15716
  }
15344
15717
 
15345
15718
  // src/bootstrap/noTsconfigProject.ts
15346
- import fs8 from "fs";
15347
- import path10 from "path";
15719
+ import fs9 from "fs";
15720
+ import path11 from "path";
15348
15721
  import {
15349
15722
  ModuleKind,
15350
15723
  ModuleResolutionKind,
@@ -15380,7 +15753,7 @@ function findSourceFiles(rootDir, depth = 0) {
15380
15753
  const found = [];
15381
15754
  let entries;
15382
15755
  try {
15383
- entries = fs8.readdirSync(rootDir, { withFileTypes: true });
15756
+ entries = fs9.readdirSync(rootDir, { withFileTypes: true });
15384
15757
  } catch {
15385
15758
  return [];
15386
15759
  }
@@ -15388,10 +15761,10 @@ function findSourceFiles(rootDir, depth = 0) {
15388
15761
  if (entry.name.startsWith(".") || SKIP_DIRECTORIES.has(entry.name)) {
15389
15762
  continue;
15390
15763
  }
15391
- const full = path10.join(rootDir, entry.name);
15764
+ const full = path11.join(rootDir, entry.name);
15392
15765
  if (entry.isDirectory()) {
15393
15766
  found.push(...findSourceFiles(full, depth + 1));
15394
- } else if (SOURCE_EXTENSIONS.has(path10.extname(entry.name)) && !entry.name.endsWith(".d.ts")) {
15767
+ } else if (SOURCE_EXTENSIONS.has(path11.extname(entry.name)) && !entry.name.endsWith(".d.ts")) {
15395
15768
  found.push(full);
15396
15769
  }
15397
15770
  }
@@ -15426,15 +15799,15 @@ function createProjectWithoutTsconfig(rootDir) {
15426
15799
  return { project, files };
15427
15800
  }
15428
15801
  function findNearestTsconfig(startDir) {
15429
- let dir = path10.resolve(startDir);
15802
+ let dir = path11.resolve(startDir);
15430
15803
  while (true) {
15431
15804
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
15432
- const candidate = path10.join(dir, name);
15433
- if (fs8.existsSync(candidate)) {
15805
+ const candidate = path11.join(dir, name);
15806
+ if (fs9.existsSync(candidate)) {
15434
15807
  return candidate;
15435
15808
  }
15436
15809
  }
15437
- const parent = path10.dirname(dir);
15810
+ const parent = path11.dirname(dir);
15438
15811
  if (parent === dir) {
15439
15812
  return null;
15440
15813
  }
@@ -15443,7 +15816,7 @@ function findNearestTsconfig(startDir) {
15443
15816
  }
15444
15817
 
15445
15818
  // src/facts/why.ts
15446
- import path11 from "path";
15819
+ import path12 from "path";
15447
15820
  import { Node as Node59, Project as Project3, SyntaxKind as SyntaxKind13 } from "ts-morph";
15448
15821
  import { explainResolutionProof, renderExplanation } from "@suss/resolution";
15449
15822
  var JS_PHRASES = {
@@ -15456,7 +15829,7 @@ var WhySession = class {
15456
15829
  store;
15457
15830
  root;
15458
15831
  constructor(options) {
15459
- this.root = path11.resolve(options.dir);
15832
+ this.root = path12.resolve(options.dir);
15460
15833
  const tsconfig = findNearestTsconfig(this.root);
15461
15834
  this.project = tsconfig === null ? createProjectWithoutTsconfig(this.root).project : new Project3({ tsConfigFilePath: tsconfig });
15462
15835
  this.store = new ResolutionStore();
@@ -15545,7 +15918,7 @@ var WhySession = class {
15545
15918
  };
15546
15919
  }
15547
15920
  sourceFileAt(file) {
15548
- const absolute = path11.resolve(this.root, file);
15921
+ const absolute = path12.resolve(this.root, file);
15549
15922
  const loaded = this.project.getSourceFile(absolute);
15550
15923
  if (loaded !== void 0) {
15551
15924
  return loaded;
@@ -15559,7 +15932,7 @@ var WhySession = class {
15559
15932
  /** A module key: a file path said relative to the root, or a package name. */
15560
15933
  displayPath(key) {
15561
15934
  if (key.startsWith(this.root)) {
15562
- return path11.relative(this.root, key);
15935
+ return path12.relative(this.root, key);
15563
15936
  }
15564
15937
  return key;
15565
15938
  }
@@ -15630,6 +16003,17 @@ function funnelDrops(packs) {
15630
16003
  }
15631
16004
  return violations;
15632
16005
  }
16006
+ function recognizersWithNoUnits(packs) {
16007
+ return packs.filter(
16008
+ (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
16009
+ // roots, so effects recognized there mean the run worked and
16010
+ // the missing framework pack costs attribution, not existence.
16011
+ funnel.effectsRecognized === 0
16012
+ ).map((funnel) => ({
16013
+ label: funnel.pack,
16014
+ 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.`
16015
+ }));
16016
+ }
15633
16017
  function unversionedPacks(packs) {
15634
16018
  return packs.filter((funnel) => funnel.version === null).map((funnel) => ({
15635
16019
  label: funnel.pack,
@@ -15724,6 +16108,12 @@ function evaluatePackHealth(report) {
15724
16108
  audience: "run",
15725
16109
  violations: selfCollisions(report.packs)
15726
16110
  },
16111
+ {
16112
+ name: "every recognizer had units to look inside",
16113
+ code: "no-units",
16114
+ audience: "run",
16115
+ violations: recognizersWithNoUnits(report.packs)
16116
+ },
15727
16117
  {
15728
16118
  name: "every pack declares a version",
15729
16119
  code: "no-version",