@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.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);
@@ -2688,7 +2688,7 @@ var ORIGIN = {
2688
2688
  declaredBy: (origin, receiver) => receiver.callee !== null && origin.importedFrom.some(
2689
2689
  (module2) => methodDeclaredIn(receiver.callee, module2, receiver.resolve)
2690
2690
  ),
2691
- constructed: (origin, receiver) => madeFromNamed(origin, madeBy(receiver)) && origin.importedFrom.some(
2691
+ constructed: (origin, receiver) => madeFromNamed(origin, madeExpression(receiver)) && origin.importedFrom.some(
2692
2692
  (module2) => isImportedFrom(madeBy(receiver), module2)
2693
2693
  )
2694
2694
  };
@@ -2696,14 +2696,15 @@ function madeFromNamed(origin, made) {
2696
2696
  if (origin.origin !== "constructed" || origin.named === void 0) {
2697
2697
  return true;
2698
2698
  }
2699
- return origin.named.includes(made.getText());
2699
+ return origin.named.includes(constructorName(made));
2700
+ }
2701
+ function constructorName(made) {
2702
+ const expression = import_ts_morph14.Node.isNewExpression(made) || import_ts_morph14.Node.isCallExpression(made) ? made.getExpression() : made;
2703
+ return import_ts_morph14.Node.isPropertyAccessExpression(expression) ? expression.getName() : expression.getText();
2700
2704
  }
2701
2705
  var CALL_ORIGIN = {
2702
2706
  declaredBy: ORIGIN.declaredBy,
2703
- constructed: (origin, receiver) => madeFromNamed(
2704
- origin,
2705
- rootIdentifier(receiver.expression) ?? receiver.expression
2706
- ) && origin.importedFrom.some(
2707
+ constructed: (origin, receiver) => madeFromNamed(origin, receiver.expression) && origin.importedFrom.some(
2707
2708
  (module2) => isImportedFrom(
2708
2709
  rootIdentifier(receiver.expression) ?? receiver.expression,
2709
2710
  module2
@@ -2711,14 +2712,19 @@ var CALL_ORIGIN = {
2711
2712
  )
2712
2713
  };
2713
2714
  function madeBy(receiver) {
2715
+ const made = madeExpression(receiver);
2716
+ const expression = import_ts_morph14.Node.isNewExpression(made) || import_ts_morph14.Node.isCallExpression(made) ? made.getExpression() : made;
2717
+ return rootIdentifier(expression) ?? expression;
2718
+ }
2719
+ function madeExpression(receiver) {
2714
2720
  if (receiver.callee === null) {
2715
- return rootIdentifier(receiver.expression) ?? receiver.expression;
2721
+ return receiver.expression;
2716
2722
  }
2717
2723
  const written = receiver.resolve(receiver.callee.getExpression());
2718
2724
  if (written === null || !import_ts_morph14.Node.isNewExpression(written) && !import_ts_morph14.Node.isCallExpression(written)) {
2719
2725
  return receiver.callee;
2720
2726
  }
2721
- return rootIdentifier(written.getExpression()) ?? receiver.callee;
2727
+ return written;
2722
2728
  }
2723
2729
  function callOpsFor(call, resolveWrittenValue) {
2724
2730
  const resolve2 = resolveWrittenValue ?? (() => null);
@@ -2759,6 +2765,7 @@ function valueOpsFor(value, resolve2) {
2759
2765
  flag: () => literalFlag(written()),
2760
2766
  entries: (unsettled) => entriesOf(written(), unsettled, resolve2),
2761
2767
  items: () => itemsOf(written(), resolve2),
2768
+ asArg: () => effectArgOf(written()),
2762
2769
  property: (name) => {
2763
2770
  const object = written();
2764
2771
  const inside = import_ts_morph14.Node.isObjectLiteralExpression(object) ? initializerOf(object, name) : null;
@@ -3246,6 +3253,9 @@ var MAX_ARG_DEPTH = 8;
3246
3253
  function extractArgs(call) {
3247
3254
  return call.getArguments().map((arg) => extractArg(arg, MAX_ARG_DEPTH));
3248
3255
  }
3256
+ function effectArgOf(node) {
3257
+ return extractArg(node, MAX_ARG_DEPTH);
3258
+ }
3249
3259
  function extractArg(node, depth) {
3250
3260
  if (import_ts_morph15.Node.isAsExpression(node) || import_ts_morph15.Node.isTypeAssertion(node) || import_ts_morph15.Node.isSatisfiesExpression(node) || import_ts_morph15.Node.isNonNullExpression(node)) {
3251
3261
  return extractArg(node.getExpression(), depth);
@@ -5838,9 +5848,9 @@ function createSourceFileLookup(project) {
5838
5848
  if (sf === null) {
5839
5849
  return null;
5840
5850
  }
5841
- const path12 = sf.getFilePath();
5842
- const index = functionsByFile.get(path12) ?? indexFunctions(sf);
5843
- functionsByFile.set(path12, index);
5851
+ const path13 = sf.getFilePath();
5852
+ const index = functionsByFile.get(path13) ?? indexFunctions(sf);
5853
+ functionsByFile.set(path13, index);
5844
5854
  return index.get(rangeKey(location.range)) ?? null;
5845
5855
  }
5846
5856
  };
@@ -6367,7 +6377,7 @@ function extractEndpointContract(endpointNode, pattern, framework) {
6367
6377
  }
6368
6378
  const responses = [];
6369
6379
  let method;
6370
- let path12;
6380
+ let path13;
6371
6381
  for (const prop of endpointNode.getProperties()) {
6372
6382
  if (!import_ts_morph28.Node.isPropertyAssignment(prop)) {
6373
6383
  continue;
@@ -6382,7 +6392,7 @@ function extractEndpointContract(endpointNode, pattern, framework) {
6382
6392
  if (propName === pattern.pathProperty) {
6383
6393
  const val = prop.getInitializer();
6384
6394
  if (val !== void 0 && import_ts_morph28.Node.isStringLiteral(val)) {
6385
- path12 = val.getLiteralValue();
6395
+ path13 = val.getLiteralValue();
6386
6396
  }
6387
6397
  }
6388
6398
  if (propName === pattern.responseExtraction.property) {
@@ -6406,10 +6416,10 @@ function extractEndpointContract(endpointNode, pattern, framework) {
6406
6416
  if (responses.length === 0) {
6407
6417
  return null;
6408
6418
  }
6409
- 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)({
6410
6420
  transport: "http",
6411
6421
  method: method ?? null,
6412
- path: path12 ?? null,
6422
+ path: path13 ?? null,
6413
6423
  recognition: framework
6414
6424
  }) : null;
6415
6425
  return {
@@ -7871,6 +7881,31 @@ function constructionRef(node, localNames, resolution) {
7871
7881
  // src/discovery/clientCall.ts
7872
7882
  var import_node_path4 = __toESM(require("path"), 1);
7873
7883
  var import_ts_morph34 = require("ts-morph");
7884
+
7885
+ // src/discovery/resolveImport.ts
7886
+ function resolveImportedLocalName(sourceFile, importModule, importName) {
7887
+ for (const importDecl of sourceFile.getImportDeclarations()) {
7888
+ if (importDecl.getModuleSpecifierValue() !== importModule) {
7889
+ continue;
7890
+ }
7891
+ for (const named2 of importDecl.getNamedImports()) {
7892
+ if (named2.getName() === importName || named2.getAliasNode()?.getText() === importName) {
7893
+ return named2.getAliasNode()?.getText() ?? named2.getName();
7894
+ }
7895
+ }
7896
+ const defaultImport = importDecl.getDefaultImport();
7897
+ if (defaultImport !== void 0 && defaultImport.getText() === importName) {
7898
+ return defaultImport.getText();
7899
+ }
7900
+ const namespaceImport = importDecl.getNamespaceImport();
7901
+ if (namespaceImport !== void 0 && namespaceImport.getText() === importName) {
7902
+ return namespaceImport.getText();
7903
+ }
7904
+ }
7905
+ return null;
7906
+ }
7907
+
7908
+ // src/discovery/clientCall.ts
7874
7909
  function discoverClientCalls(sourceFile, match, kind, resolution) {
7875
7910
  const results = [];
7876
7911
  const isGlobal = match.importModule === "global";
@@ -7889,7 +7924,7 @@ function discoverClientCalls(sourceFile, match, kind, resolution) {
7889
7924
  if (!isGlobal && importedLocalName !== null) {
7890
7925
  for (const varDecl of sourceFile.getVariableDeclarations()) {
7891
7926
  const init = varDecl.getInitializer();
7892
- if (init !== void 0 && import_ts_morph34.Node.isCallExpression(init) && isInstanceCreationCall(init, importedLocalName, match.factoryMethods)) {
7927
+ if (init !== void 0 && (import_ts_morph34.Node.isCallExpression(init) || import_ts_morph34.Node.isNewExpression(init)) && isInstanceCreationCall(init, importedLocalName, match.factoryMethods)) {
7893
7928
  clientVarNames.add(varDecl.getName());
7894
7929
  }
7895
7930
  }
@@ -7934,12 +7969,50 @@ function discoverClientCalls(sourceFile, match, kind, resolution) {
7934
7969
  });
7935
7970
  return results;
7936
7971
  }
7972
+ function clientReceiverCheckFor(sourceFile, match, resolution) {
7973
+ const localName = resolveImportedLocalName(
7974
+ sourceFile,
7975
+ match.importModule,
7976
+ match.importName
7977
+ );
7978
+ const constructedHere = /* @__PURE__ */ new Set();
7979
+ if (localName !== null) {
7980
+ sourceFile.forEachDescendant((node) => {
7981
+ if (import_ts_morph34.Node.isVariableDeclaration(node)) {
7982
+ const init = node.getInitializer();
7983
+ if (init !== void 0 && (import_ts_morph34.Node.isCallExpression(init) || import_ts_morph34.Node.isNewExpression(init)) && isInstanceCreationCall(init, localName, match.factoryMethods) || typedAsClient(node.getTypeNode(), localName)) {
7984
+ constructedHere.add(node.getName());
7985
+ }
7986
+ return;
7987
+ }
7988
+ if (import_ts_morph34.Node.isParameterDeclaration(node) && typedAsClient(node.getTypeNode(), localName)) {
7989
+ const written = node.getNameNode();
7990
+ if (import_ts_morph34.Node.isIdentifier(written)) {
7991
+ constructedHere.add(written.getText());
7992
+ }
7993
+ }
7994
+ });
7995
+ }
7996
+ const full = {
7997
+ type: "clientCall",
7998
+ importModule: match.importModule,
7999
+ importName: match.importName,
8000
+ ...match.factoryMethods === void 0 ? {} : { factoryMethods: match.factoryMethods }
8001
+ };
8002
+ return (subject) => localName !== null && import_ts_morph34.Node.isIdentifier(subject) && subject.getText() === localName || import_ts_morph34.Node.isIdentifier(subject) && constructedHere.has(subject.getText()) || resolvesToKnownInstance(subject, full, resolution);
8003
+ }
8004
+ function typedAsClient(typeNode, localName) {
8005
+ if (typeNode === void 0 || !import_ts_morph34.Node.isTypeReference(typeNode)) {
8006
+ return false;
8007
+ }
8008
+ return typeNode.getTypeName().getText() === localName;
8009
+ }
7937
8010
  function resolvesToKnownInstance(subject, match, resolution) {
7938
8011
  if (resolution === void 0 || match.importModule === "global") {
7939
8012
  return false;
7940
8013
  }
7941
8014
  const written = writtenNodeOf(subject, resolution);
7942
- if (written === null || !import_ts_morph34.Node.isCallExpression(written)) {
8015
+ if (written === null || !import_ts_morph34.Node.isCallExpression(written) && !import_ts_morph34.Node.isNewExpression(written)) {
7943
8016
  return false;
7944
8017
  }
7945
8018
  const writtenLocalName = resolvedImportLocalName(
@@ -8261,7 +8334,34 @@ function joinRoutePath(prefix, suffix) {
8261
8334
  }
8262
8335
  return `/${segments.join("/")}`;
8263
8336
  }
8264
- 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) {
8265
8365
  const acceptedModules = Array.isArray(match.importModule) ? match.importModule : [match.importModule];
8266
8366
  const routeDecoratorNames = Object.keys(match.methodDecoratorRouteMap);
8267
8367
  const importedRouteDecorators = /* @__PURE__ */ new Set();
@@ -8293,6 +8393,22 @@ function discoverDecoratedRoutes(sourceFile, match, kind, resolution) {
8293
8393
  const pathPrefix = routePathOf(marker.args, resolution);
8294
8394
  const className = cls.getName() ?? "<anon-class>";
8295
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
+ }
8296
8412
  const httpMethod = match.methodDecoratorRouteMap[handler.standsFor];
8297
8413
  const pathSuffix = resolveRoutePathArg(handler.decorator, resolution);
8298
8414
  const routePath = joinRoutePath(pathPrefix, pathSuffix);
@@ -9134,40 +9250,8 @@ function discoverGraphqlHookCalls(sourceFile, match, kind, resolution) {
9134
9250
 
9135
9251
  // src/discovery/graphqlImperativeCall.ts
9136
9252
  var import_ts_morph42 = require("ts-morph");
9137
-
9138
- // src/discovery/resolveImport.ts
9139
- function resolveImportedLocalName(sourceFile, importModule, importName) {
9140
- for (const importDecl of sourceFile.getImportDeclarations()) {
9141
- if (importDecl.getModuleSpecifierValue() !== importModule) {
9142
- continue;
9143
- }
9144
- for (const named2 of importDecl.getNamedImports()) {
9145
- if (named2.getName() === importName || named2.getAliasNode()?.getText() === importName) {
9146
- return named2.getAliasNode()?.getText() ?? named2.getName();
9147
- }
9148
- }
9149
- const defaultImport = importDecl.getDefaultImport();
9150
- if (defaultImport !== void 0 && defaultImport.getText() === importName) {
9151
- return defaultImport.getText();
9152
- }
9153
- const namespaceImport = importDecl.getNamespaceImport();
9154
- if (namespaceImport !== void 0 && namespaceImport.getText() === importName) {
9155
- return namespaceImport.getText();
9156
- }
9157
- }
9158
- return null;
9159
- }
9160
-
9161
- // src/discovery/graphqlImperativeCall.ts
9162
9253
  function discoverGraphqlImperativeCalls(sourceFile, match, kind, resolution) {
9163
- const localName = resolveImportedLocalName(
9164
- sourceFile,
9165
- match.importModule,
9166
- match.importName
9167
- );
9168
- if (localName === null) {
9169
- return [];
9170
- }
9254
+ const isTheClient = clientReceiverCheckFor(sourceFile, match, resolution);
9171
9255
  const methodSpec = /* @__PURE__ */ new Map();
9172
9256
  for (const method of match.methods) {
9173
9257
  methodSpec.set(method.methodName, {
@@ -9189,6 +9273,9 @@ function discoverGraphqlImperativeCalls(sourceFile, match, kind, resolution) {
9189
9273
  if (spec === void 0) {
9190
9274
  return;
9191
9275
  }
9276
+ if (!isTheClient(callee.getExpression())) {
9277
+ return;
9278
+ }
9192
9279
  const args = node.getArguments();
9193
9280
  if (args.length === 0 || !import_ts_morph42.Node.isObjectLiteralExpression(args[0])) {
9194
9281
  return;
@@ -9335,7 +9422,7 @@ function attributeValueOf(node, name) {
9335
9422
  }
9336
9423
  return null;
9337
9424
  }
9338
- function literalStringOf(value) {
9425
+ function literalStringOf2(value) {
9339
9426
  const unwrapped2 = import_ts_morph43.Node.isJsxExpression(value) ? value.getExpression() ?? value : value;
9340
9427
  if (import_ts_morph43.Node.isStringLiteral(unwrapped2) || import_ts_morph43.Node.isNoSubstitutionTemplateLiteral(unwrapped2)) {
9341
9428
  return unwrapped2.getLiteralValue();
@@ -9345,7 +9432,7 @@ function literalStringOf(value) {
9345
9432
  function ownPathOfJsx(node, match) {
9346
9433
  const pathValue = attributeValueOf(node, match.pathAttribute);
9347
9434
  if (pathValue !== null) {
9348
- const literal = literalStringOf(pathValue);
9435
+ const literal = literalStringOf2(pathValue);
9349
9436
  return literal !== null ? { kind: "literal", value: literal } : { kind: "unreadable" };
9350
9437
  }
9351
9438
  if (match.indexAttribute !== void 0 && attributeValueOf(node, match.indexAttribute) !== null) {
@@ -9436,14 +9523,14 @@ function unitForJsxRoute(node, match, routeNames, kind, resolution) {
9436
9523
  }
9437
9524
  return routeUnit(node, kind, match.method, resolved.path, target, resolution);
9438
9525
  }
9439
- function routeUnit(at, kind, method, path12, target, resolution) {
9440
- const routeInfo = { method, path: path12 };
9526
+ function routeUnit(at, kind, method, path13, target, resolution) {
9527
+ const routeInfo = { method, path: path13 };
9441
9528
  if (target.kind === "unreadable") {
9442
9529
  return {
9443
9530
  func: null,
9444
9531
  announcedAt: at,
9445
9532
  kind,
9446
- name: path12,
9533
+ name: path13,
9447
9534
  nameKind: "label",
9448
9535
  routeInfo
9449
9536
  };
@@ -9613,7 +9700,7 @@ function unitsUnderRouteObject(object, match, kind, resolution, prefix, ancestry
9613
9700
  function ownPathOfObject(object, match) {
9614
9701
  const pathValue = routePropertyOf(object, match.pathAttribute);
9615
9702
  if (pathValue !== null) {
9616
- const literal = literalStringOf(pathValue);
9703
+ const literal = literalStringOf2(pathValue);
9617
9704
  return literal !== null ? { kind: "literal", value: literal } : { kind: "unreadable" };
9618
9705
  }
9619
9706
  if (match.indexAttribute !== void 0) {
@@ -9873,6 +9960,9 @@ function resolvePackageExportsCached(packageJsonPath) {
9873
9960
  return fresh;
9874
9961
  }
9875
9962
  function discoverPackageExports(sourceFile, match, kind) {
9963
+ if (match.packageJsonPath === void 0) {
9964
+ return [];
9965
+ }
9876
9966
  const { entries } = resolvePackageExportsCached(match.packageJsonPath);
9877
9967
  const filePath = sourceFile.getFilePath();
9878
9968
  const matching = [];
@@ -10260,12 +10350,12 @@ function originEndOf(flattened) {
10260
10350
  const slash = flattened.indexOf("/", opener[0].length);
10261
10351
  return slash === -1 ? flattened.length : slash;
10262
10352
  }
10263
- function appendPathText(path12, text) {
10353
+ function appendPathText(path13, text) {
10264
10354
  const idx = text.search(/[?#]/);
10265
10355
  if (idx === -1) {
10266
- return { path: path12 + text, stop: false };
10356
+ return { path: path13 + text, stop: false };
10267
10357
  }
10268
- return { path: path12 + text.slice(0, idx), stop: true };
10358
+ return { path: path13 + text.slice(0, idx), stop: true };
10269
10359
  }
10270
10360
  function parseAbsoluteUrl(text) {
10271
10361
  try {
@@ -10289,8 +10379,8 @@ function pathnameOfAbsoluteLiteral(text) {
10289
10379
  return stripQueryAndFragment(stripOriginManually(text));
10290
10380
  }
10291
10381
  function pathFromLiteralUrl(text) {
10292
- const path12 = isAbsoluteUrlLiteral(text) ? pathnameOfAbsoluteLiteral(text) : stripQueryAndFragment(text);
10293
- return path12 === "" ? void 0 : path12;
10382
+ const path13 = isAbsoluteUrlLiteral(text) ? pathnameOfAbsoluteLiteral(text) : stripQueryAndFragment(text);
10383
+ return path13 === "" ? void 0 : path13;
10294
10384
  }
10295
10385
  function pathFromTemplateUrl(arg, resolution) {
10296
10386
  const headText = arg.getHead().getLiteralText();
@@ -10298,7 +10388,7 @@ function pathFromTemplateUrl(arg, resolution) {
10298
10388
  const tails = spans.map((span) => span.getLiteral().getLiteralText());
10299
10389
  const originEnd = originEndOf([headText, ...tails].join(SUBSTITUTION));
10300
10390
  const head = appendPathText("", headText.slice(originEnd));
10301
- let path12 = head.path;
10391
+ let path13 = head.path;
10302
10392
  let stop = head.stop;
10303
10393
  let at = headText.length;
10304
10394
  for (let i = 0; i < spans.length && !stop; i++) {
@@ -10308,18 +10398,18 @@ function pathFromTemplateUrl(arg, resolution) {
10308
10398
  continue;
10309
10399
  }
10310
10400
  if (at >= originEnd) {
10311
- path12 += holeText(span.getExpression(), resolution);
10401
+ path13 += holeText(span.getExpression(), resolution);
10312
10402
  }
10313
10403
  at += SUBSTITUTION.length;
10314
10404
  const appended = appendPathText(
10315
- path12,
10405
+ path13,
10316
10406
  tailText.slice(Math.max(0, originEnd - at))
10317
10407
  );
10318
- path12 = appended.path;
10408
+ path13 = appended.path;
10319
10409
  stop = appended.stop;
10320
10410
  at += tailText.length;
10321
10411
  }
10322
- return path12 === "" ? void 0 : path12;
10412
+ return path13 === "" ? void 0 : path13;
10323
10413
  }
10324
10414
  function pathFromUrlNode(node, resolution) {
10325
10415
  if (import_ts_morph48.Node.isStringLiteral(node)) {
@@ -10331,8 +10421,24 @@ function pathFromUrlNode(node, resolution) {
10331
10421
  if (import_ts_morph48.Node.isTemplateExpression(node)) {
10332
10422
  return pathFromTemplateUrl(node, resolution);
10333
10423
  }
10424
+ if (import_ts_morph48.Node.isBinaryExpression(node)) {
10425
+ const joined = joinedStringOf(node, resolution);
10426
+ return joined === void 0 ? void 0 : pathFromLiteralUrl(joined);
10427
+ }
10334
10428
  return void 0;
10335
10429
  }
10430
+ function joinedStringOf(node, resolution) {
10431
+ if (import_ts_morph48.Node.isStringLiteral(node) || import_ts_morph48.Node.isNoSubstitutionTemplateLiteral(node)) {
10432
+ return node.getLiteralValue();
10433
+ }
10434
+ if (import_ts_morph48.Node.isBinaryExpression(node) && node.getOperatorToken().getText() === "+") {
10435
+ const left = joinedStringOf(node.getLeft(), resolution);
10436
+ const right = joinedStringOf(node.getRight(), resolution);
10437
+ return left === void 0 || right === void 0 ? void 0 : left + right;
10438
+ }
10439
+ const written = resolution?.resolveWrittenValue(node) ?? null;
10440
+ return written === null || written === node ? void 0 : joinedStringOf(written, resolution);
10441
+ }
10336
10442
  function placeholderName(expr) {
10337
10443
  if (import_ts_morph48.Node.isIdentifier(expr)) {
10338
10444
  return expr.getText();
@@ -10470,9 +10576,9 @@ function withMountPrefix(routeInfo, subjectNode, mountPrefixes) {
10470
10576
  const prefix = mountPrefixes.effectivePrefixFor(subjectNode);
10471
10577
  return prefix === "" ? routeInfo : { ...routeInfo, path: joinMountedPath(prefix, routeInfo.path) };
10472
10578
  }
10473
- function joinMountedPath(prefix, path12) {
10579
+ function joinMountedPath(prefix, path13) {
10474
10580
  const trimmed = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
10475
- return trimmed === "" ? path12 : trimmed + path12;
10581
+ return trimmed === "" ? path13 : trimmed + path13;
10476
10582
  }
10477
10583
  function registrationSubjectsOf(sourceFile, importModule, importName) {
10478
10584
  let importedLocalName = null;
@@ -10636,11 +10742,11 @@ function extractRouteInfoFromBinding(call, methodName, binding, resolution) {
10636
10742
  return null;
10637
10743
  }
10638
10744
  const method2 = stringProperty(routeObject, binding.method.property);
10639
- const path13 = stringProperty(routeObject, binding.path.property);
10640
- if (method2 === null || path13 === null) {
10745
+ const path14 = stringProperty(routeObject, binding.path.property);
10746
+ if (method2 === null || path14 === null) {
10641
10747
  return null;
10642
10748
  }
10643
- return { method: method2.toUpperCase(), path: path13 };
10749
+ return { method: method2.toUpperCase(), path: path14 };
10644
10750
  }
10645
10751
  if (binding.method.type !== "fromRegistration" || binding.path.type !== "fromArgument") {
10646
10752
  return null;
@@ -10657,26 +10763,46 @@ function extractRouteInfoFromBinding(call, methodName, binding, resolution) {
10657
10763
  method = arg.getLiteralValue().toUpperCase();
10658
10764
  }
10659
10765
  const pathArg = call.getArguments()[binding.path.position];
10660
- const path12 = pathArg === void 0 ? void 0 : pathFromArgument(pathArg, resolution);
10661
- 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 };
10662
10768
  }
10663
10769
 
10664
10770
  // src/discovery/registrationLoop.ts
10665
10771
  var import_ts_morph50 = require("ts-morph");
10666
10772
  function discoverRegistrationLoops(sourceFile, match, kind, resolution) {
10667
10773
  const results = [];
10774
+ const routables = routableNames(sourceFile, match);
10775
+ if (routables !== null && routables.size === 0) {
10776
+ return results;
10777
+ }
10668
10778
  sourceFile.forEachDescendant((node) => {
10669
10779
  if (!import_ts_morph50.Node.isForOfStatement(node)) {
10670
10780
  return;
10671
10781
  }
10672
- const expanded = tryExpandLoop(node, match, kind, resolution);
10782
+ const expanded = tryExpandLoop(node, match, kind, resolution, routables);
10673
10783
  if (expanded !== null) {
10674
10784
  results.push(...expanded);
10675
10785
  }
10676
10786
  });
10677
10787
  return results;
10678
10788
  }
10679
- function tryExpandLoop(loop, match, kind, resolution) {
10789
+ function routableNames(sourceFile, match) {
10790
+ if (match.receiver === void 0) {
10791
+ return null;
10792
+ }
10793
+ const names = /* @__PURE__ */ new Set();
10794
+ for (const importName of match.receiver.importNames) {
10795
+ for (const name of registrationSubjectsOf(
10796
+ sourceFile,
10797
+ match.receiver.importModule,
10798
+ importName
10799
+ ).keys()) {
10800
+ names.add(name);
10801
+ }
10802
+ }
10803
+ return names;
10804
+ }
10805
+ function tryExpandLoop(loop, match, kind, resolution, routables) {
10680
10806
  const loopVar = loopVariableName(loop);
10681
10807
  if (loopVar === null) {
10682
10808
  return null;
@@ -10684,6 +10810,9 @@ function tryExpandLoop(loop, match, kind, resolution) {
10684
10810
  if (!bodyReferencesLoopVar(loop, loopVar)) {
10685
10811
  return null;
10686
10812
  }
10813
+ if (routables !== null && !bodyCallsOn(loop, routables)) {
10814
+ return null;
10815
+ }
10687
10816
  const arrayLit = arrayLiteralOf(loop.getExpression(), resolution);
10688
10817
  if (arrayLit === null) {
10689
10818
  return null;
@@ -10742,9 +10871,23 @@ function bodyReferencesLoopVar(loop, name) {
10742
10871
  });
10743
10872
  return referenced;
10744
10873
  }
10874
+ function bodyCallsOn(loop, routables) {
10875
+ let found = false;
10876
+ loop.getStatement().forEachDescendant((node) => {
10877
+ if (found || !import_ts_morph50.Node.isCallExpression(node)) {
10878
+ return;
10879
+ }
10880
+ const callee = node.getExpression();
10881
+ const subject = import_ts_morph50.Node.isPropertyAccessExpression(callee) || import_ts_morph50.Node.isElementAccessExpression(callee) ? callee.getExpression() : null;
10882
+ if (subject !== null && import_ts_morph50.Node.isIdentifier(subject) && routables.has(subject.getText())) {
10883
+ found = true;
10884
+ }
10885
+ });
10886
+ return found;
10887
+ }
10745
10888
  function readRouteSpec(element, shape, resolution) {
10746
10889
  let method = null;
10747
- let path12 = null;
10890
+ let path13 = null;
10748
10891
  let handler = null;
10749
10892
  for (const prop of element.getProperties()) {
10750
10893
  if (import_ts_morph50.Node.isMethodDeclaration(prop) && prop.getName() === shape.handlerKey) {
@@ -10762,16 +10905,16 @@ function readRouteSpec(element, shape, resolution) {
10762
10905
  if (name === shape.methodKey) {
10763
10906
  method = readStringLiteralValue(init);
10764
10907
  } else if (name === shape.pathKey) {
10765
- path12 = readStringLiteralValue(init);
10908
+ path13 = readStringLiteralValue(init);
10766
10909
  } else if (name === shape.handlerKey) {
10767
10910
  const func = functionValueOf(init, resolution);
10768
10911
  handler = func === null ? null : { func, name: handlerName(init) };
10769
10912
  }
10770
10913
  }
10771
- if (method === null || path12 === null || handler === null) {
10914
+ if (method === null || path13 === null || handler === null) {
10772
10915
  return null;
10773
10916
  }
10774
- return { method, path: path12, handler: handler.func, handlerName: handler.name };
10917
+ return { method, path: path13, handler: handler.func, handlerName: handler.name };
10775
10918
  }
10776
10919
  function readStringLiteralValue(node) {
10777
10920
  if (import_ts_morph50.Node.isStringLiteral(node) || import_ts_morph50.Node.isNoSubstitutionTemplateLiteral(node)) {
@@ -10800,8 +10943,8 @@ function discoverRegistrationTemplates(sourceFile, match, kind, resolution) {
10800
10943
  }
10801
10944
  const args = node.getArguments();
10802
10945
  for (const reg of match.registrations) {
10803
- const path12 = substitutePath(reg.pathTemplate, args);
10804
- if (path12 === null) {
10946
+ const path13 = substitutePath(reg.pathTemplate, args);
10947
+ if (path13 === null) {
10805
10948
  continue;
10806
10949
  }
10807
10950
  const handler = resolveHandler(reg.handlerArg, args, resolution);
@@ -10814,7 +10957,7 @@ function discoverRegistrationTemplates(sourceFile, match, kind, resolution) {
10814
10957
  name: handler.name,
10815
10958
  routeInfo: {
10816
10959
  method: reg.method.toUpperCase(),
10817
- path: path12
10960
+ path: path13
10818
10961
  }
10819
10962
  });
10820
10963
  }
@@ -11140,7 +11283,8 @@ function runPattern(sourceFile, pattern, resolution, mountPrefixes) {
11140
11283
  sourceFile,
11141
11284
  pattern.match,
11142
11285
  pattern.kind,
11143
- resolution
11286
+ resolution,
11287
+ pattern.binding
11144
11288
  );
11145
11289
  }
11146
11290
  if (pattern.match.type === "jsxElementRoute") {
@@ -12405,7 +12549,38 @@ var REACHABLE_RULES = [
12405
12549
  )
12406
12550
  ];
12407
12551
  var NO_RECOGNIZERS = { invocation: [], access: [] };
12408
- 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 = []) {
12409
12584
  const lookup = createSourceFileLookup(project);
12410
12585
  const db = facts?.db ?? new import_datalog4.Database();
12411
12586
  const functionByKey = /* @__PURE__ */ new Map();
@@ -12425,6 +12600,13 @@ function expandReachableClosure(seeds, project, options, projectFileSet, facts,
12425
12600
  db.add("entry", [key]);
12426
12601
  }
12427
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
+ }
12428
12610
  const knownKeys = /* @__PURE__ */ new Set();
12429
12611
  for (const summary of alreadySummarized) {
12430
12612
  const func = lookup.functionAt(summary.location);
@@ -13486,6 +13668,185 @@ function computeAdapterPacksDigest(packVersions) {
13486
13668
  return `${adapterStamp}|${sortedPacks.join(",")}`;
13487
13669
  }
13488
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
+
13489
13850
  // src/adapter.ts
13490
13851
  var raise = (msg) => {
13491
13852
  throw new Error(msg);
@@ -13896,11 +14257,11 @@ function extractConsumerBinding(unit, pattern, pack, resolution) {
13896
14257
  return null;
13897
14258
  }
13898
14259
  const method = extractBindingMethod(binding, callSite, pack, resolution);
13899
- const path12 = extractBindingPath(binding, callSite, pack, resolution);
14260
+ const path13 = extractBindingPath(binding, callSite, pack, resolution);
13900
14261
  return (0, import_behavioral_ir8.restBinding)({
13901
14262
  transport: pack.protocol,
13902
14263
  method: method ?? null,
13903
- path: path12 ?? null,
14264
+ path: path13 ?? null,
13904
14265
  recognition: pack.name
13905
14266
  });
13906
14267
  }
@@ -14512,8 +14873,8 @@ function synthesizeCallerSummaries(wrapper, _project, options, resolution) {
14512
14873
  if (pathArg === void 0) {
14513
14874
  continue;
14514
14875
  }
14515
- const path12 = pathFromArgument(pathArg, resolution);
14516
- if (path12 === void 0) {
14876
+ const path13 = pathFromArgument(pathArg, resolution);
14877
+ if (path13 === void 0) {
14517
14878
  continue;
14518
14879
  }
14519
14880
  const callerFunc = enclosingFunction(callExpr);
@@ -14525,7 +14886,7 @@ function synthesizeCallerSummaries(wrapper, _project, options, resolution) {
14525
14886
  continue;
14526
14887
  }
14527
14888
  seen.add(dedupKey);
14528
- out.push(buildCallerSummary(wrapper, callerFunc, callExpr, path12, options));
14889
+ out.push(buildCallerSummary(wrapper, callerFunc, callExpr, path13, options));
14529
14890
  }
14530
14891
  return out;
14531
14892
  }
@@ -14582,7 +14943,7 @@ function callerName(func) {
14582
14943
  }
14583
14944
  return "anonymous";
14584
14945
  }
14585
- function buildCallerSummary(wrapper, callerFunc, callExpr, path12, options) {
14946
+ function buildCallerSummary(wrapper, callerFunc, callExpr, path13, options) {
14586
14947
  const wrapperBinding = wrapper.summary.identity.boundaryBinding;
14587
14948
  const wrapperRest = wrapperBinding?.semantics.name === "rest" ? wrapperBinding.semantics : null;
14588
14949
  const syntheticPack = {
@@ -14614,7 +14975,7 @@ function buildCallerSummary(wrapper, callerFunc, callExpr, path12, options) {
14614
14975
  raw.boundaryBinding = (0, import_behavioral_ir8.restBinding)({
14615
14976
  transport: wrapperBinding?.transport ?? "http",
14616
14977
  method: wrapperRest?.method ?? null,
14617
- path: path12,
14978
+ path: path13,
14618
14979
  recognition: wrapperBinding?.recognition ?? "unknown"
14619
14980
  });
14620
14981
  const summary = (0, import_extractor5.assembleSummary)(raw, options);
@@ -14680,25 +15041,32 @@ function loadRunFiles(project, candidatePaths, timer) {
14680
15041
  );
14681
15042
  return { sourceFiles, deep };
14682
15043
  }
14683
- 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
+ };
14684
15052
  const project = config.project ?? new import_ts_morph60.Project(
14685
15053
  config.tsConfigFilePath !== void 0 ? {
14686
15054
  tsConfigFilePath: config.tsConfigFilePath,
14687
15055
  skipAddingFilesFromTsConfig: true
14688
15056
  } : { skipAddingFilesFromTsConfig: true }
14689
15057
  );
14690
- 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);
14691
15059
  let lazyBootstrapped = false;
14692
15060
  let projectFileSet;
14693
15061
  const cacheDir = declineWhenRunFromSource(
14694
- 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)
14695
15063
  );
14696
15064
  const cache = createCacheLayer(cacheDir);
14697
15065
  const adapterPacksDigest = `${computeAdapterPacksDigest(
14698
15066
  config.frameworks.map(
14699
15067
  (p) => p.version !== void 0 ? { name: p.name, version: p.version } : { name: p.name }
14700
15068
  )
14701
- )}|${extractionConfigStamp(config)}`;
15069
+ )}|${extractionConfigStamp(config)}|ws:${workspaceExpansionStamp(config.frameworks)}`;
14702
15070
  const packWrappers = config.frameworks.flatMap(
14703
15071
  (pack) => pack.transparentWrappers ?? []
14704
15072
  );
@@ -14934,7 +15302,12 @@ function createTypeScriptAdapter(config) {
14934
15302
  },
14935
15303
  // Reached units the cache already serves emit nothing,
14936
15304
  // the way a cold run's seeds do not.
14937
- 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)
14938
15311
  )
14939
15312
  ) : withSubUnits;
14940
15313
  const closureOwnership = caching && config.includeReachable !== false ? timer.time(
@@ -15331,8 +15704,8 @@ function buildSubUnitSummary(subUnit, parent, invocationRecognizers, accessRecog
15331
15704
  }
15332
15705
 
15333
15706
  // src/bootstrap/noTsconfigProject.ts
15334
- var import_node_fs6 = __toESM(require("fs"), 1);
15335
- 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);
15336
15709
  var import_ts_morph61 = require("ts-morph");
15337
15710
  var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
15338
15711
  ".ts",
@@ -15363,7 +15736,7 @@ function findSourceFiles(rootDir, depth = 0) {
15363
15736
  const found = [];
15364
15737
  let entries;
15365
15738
  try {
15366
- entries = import_node_fs6.default.readdirSync(rootDir, { withFileTypes: true });
15739
+ entries = import_node_fs7.default.readdirSync(rootDir, { withFileTypes: true });
15367
15740
  } catch {
15368
15741
  return [];
15369
15742
  }
@@ -15371,10 +15744,10 @@ function findSourceFiles(rootDir, depth = 0) {
15371
15744
  if (entry.name.startsWith(".") || SKIP_DIRECTORIES.has(entry.name)) {
15372
15745
  continue;
15373
15746
  }
15374
- const full = import_node_path10.default.join(rootDir, entry.name);
15747
+ const full = import_node_path11.default.join(rootDir, entry.name);
15375
15748
  if (entry.isDirectory()) {
15376
15749
  found.push(...findSourceFiles(full, depth + 1));
15377
- } 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")) {
15378
15751
  found.push(full);
15379
15752
  }
15380
15753
  }
@@ -15409,15 +15782,15 @@ function createProjectWithoutTsconfig(rootDir) {
15409
15782
  return { project, files };
15410
15783
  }
15411
15784
  function findNearestTsconfig(startDir) {
15412
- let dir = import_node_path10.default.resolve(startDir);
15785
+ let dir = import_node_path11.default.resolve(startDir);
15413
15786
  while (true) {
15414
15787
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
15415
- const candidate = import_node_path10.default.join(dir, name);
15416
- 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)) {
15417
15790
  return candidate;
15418
15791
  }
15419
15792
  }
15420
- const parent = import_node_path10.default.dirname(dir);
15793
+ const parent = import_node_path11.default.dirname(dir);
15421
15794
  if (parent === dir) {
15422
15795
  return null;
15423
15796
  }
@@ -15426,7 +15799,7 @@ function findNearestTsconfig(startDir) {
15426
15799
  }
15427
15800
 
15428
15801
  // src/facts/why.ts
15429
- var import_node_path11 = __toESM(require("path"), 1);
15802
+ var import_node_path12 = __toESM(require("path"), 1);
15430
15803
  var import_ts_morph62 = require("ts-morph");
15431
15804
  var import_resolution2 = require("@suss/resolution");
15432
15805
  var JS_PHRASES = {
@@ -15439,7 +15812,7 @@ var WhySession = class {
15439
15812
  store;
15440
15813
  root;
15441
15814
  constructor(options) {
15442
- this.root = import_node_path11.default.resolve(options.dir);
15815
+ this.root = import_node_path12.default.resolve(options.dir);
15443
15816
  const tsconfig = findNearestTsconfig(this.root);
15444
15817
  this.project = tsconfig === null ? createProjectWithoutTsconfig(this.root).project : new import_ts_morph62.Project({ tsConfigFilePath: tsconfig });
15445
15818
  this.store = new ResolutionStore();
@@ -15528,7 +15901,7 @@ var WhySession = class {
15528
15901
  };
15529
15902
  }
15530
15903
  sourceFileAt(file) {
15531
- const absolute = import_node_path11.default.resolve(this.root, file);
15904
+ const absolute = import_node_path12.default.resolve(this.root, file);
15532
15905
  const loaded = this.project.getSourceFile(absolute);
15533
15906
  if (loaded !== void 0) {
15534
15907
  return loaded;
@@ -15542,7 +15915,7 @@ var WhySession = class {
15542
15915
  /** A module key: a file path said relative to the root, or a package name. */
15543
15916
  displayPath(key) {
15544
15917
  if (key.startsWith(this.root)) {
15545
- return import_node_path11.default.relative(this.root, key);
15918
+ return import_node_path12.default.relative(this.root, key);
15546
15919
  }
15547
15920
  return key;
15548
15921
  }
@@ -15613,6 +15986,17 @@ function funnelDrops(packs) {
15613
15986
  }
15614
15987
  return violations;
15615
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
+ }
15616
16000
  function unversionedPacks(packs) {
15617
16001
  return packs.filter((funnel) => funnel.version === null).map((funnel) => ({
15618
16002
  label: funnel.pack,
@@ -15707,6 +16091,12 @@ function evaluatePackHealth(report) {
15707
16091
  audience: "run",
15708
16092
  violations: selfCollisions(report.packs)
15709
16093
  },
16094
+ {
16095
+ name: "every recognizer had units to look inside",
16096
+ code: "no-units",
16097
+ audience: "run",
16098
+ violations: recognizersWithNoUnits(report.packs)
16099
+ },
15710
16100
  {
15711
16101
  name: "every pack declares a version",
15712
16102
  code: "no-version",