@ivorycanvas/qamap 0.3.1 → 0.3.2

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/e2e.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
+ import YAML from "yaml";
3
4
  import { buildDomainLanguageSummary } from "./domain-language.js";
4
5
  import { defaultDomainManifestPath, loadDomainManifest, matchDomains } from "./domains.js";
5
6
  import { collectProjectFiles } from "./fs.js";
7
+ import { buildReverseImportIndex, expandChangedFilesWithImporters, findImportingSurfaces } from "./import-graph.js";
6
8
  import { loadCoreFlowManifest, matchCoreFlows } from "./flows.js";
7
9
  import { loadVerificationManifest, matchVerificationManifest } from "./manifest.js";
8
10
  import { collectTestSuiteInventory, evaluateFlowCoverageEvidence, summarizeTestSuiteInventory, } from "./test-evidence.js";
9
- import { generateTestPlan } from "./test-plan.js";
11
+ import { collectAddedDiffText, generateTestPlan } from "./test-plan.js";
10
12
  import { TOOL_NAME, VERSION } from "./version.js";
11
13
  const maxFilesPerFlow = 8;
12
14
  const workspacePackageSearchLimit = 200;
@@ -34,14 +36,21 @@ export async function generateE2ePlan(rootInput, options = {}) {
34
36
  const coreFlowRoot = testPlan.workspaceRoot ?? root;
35
37
  const coreFlowManifest = await loadCoreFlowManifest(coreFlowRoot);
36
38
  const coreFlowChangedFiles = toCoreFlowChangedFiles(testPlan.changedFiles, root, coreFlowRoot);
37
- const coreFlows = matchCoreFlows(coreFlowManifest, coreFlowChangedFiles);
39
+ const matchableChangedFiles = await expandChangedFilesForMatching(coreFlowRoot, coreFlowChangedFiles);
40
+ const coreFlows = matchCoreFlows(coreFlowManifest, matchableChangedFiles);
38
41
  const domainManifest = await loadDomainManifest(coreFlowRoot);
39
- const domains = matchDomains(domainManifest, coreFlowChangedFiles);
42
+ const domains = matchDomains(domainManifest, matchableChangedFiles);
40
43
  const verificationManifest = await loadVerificationManifest(coreFlowRoot, { manifestPath: options.manifestPath });
41
- const verificationManifestMatches = matchVerificationManifest(verificationManifest, coreFlowChangedFiles);
42
- const domainLanguage = await buildDomainLanguageSummary(root, testPlan.changedFiles, coreFlows, domains);
44
+ const verificationManifestMatches = matchVerificationManifest(verificationManifest, matchableChangedFiles);
45
+ const addedDiffText = await collectAddedDiffText(root, {
46
+ base: testPlan.base,
47
+ head: testPlan.head,
48
+ workspaceRoot: testPlan.workspaceRoot,
49
+ includeWorkingTree: options.includeWorkingTree,
50
+ });
51
+ const domainLanguage = await buildDomainLanguageSummary(root, testPlan.changedFiles, coreFlows, domains, addedDiffText);
43
52
  const workspaceTargets = await buildWorkspaceTargets(root, testPlan);
44
- const flows = await buildFlows(root, testPlan.changedFiles, recommendedRunner.name, project.type, testSuiteInventory, domainLanguage);
53
+ const flows = await buildFlows(root, testPlan.changedFiles, recommendedRunner.name, project.type, testSuiteInventory, domainLanguage, addedDiffText);
45
54
  const testSuite = summarizeTestSuiteInventory(testSuiteInventory);
46
55
  const missingTestability = uniqueStrings([
47
56
  ...flows.flatMap((flow) => flow.missingTestability),
@@ -1214,7 +1223,7 @@ function inferFlowActor(flow) {
1214
1223
  if (/\b(auth|login|logout|session|account|profile|permission)\b/.test(haystack)) {
1215
1224
  return "Signed-in user or guest";
1216
1225
  }
1217
- if (/\b(checkout|purchase|payment|order|cart|offer|subscription|membership|billing)\b/.test(haystack)) {
1226
+ if (/\b(checkout|purchase|payment|order|cart|offer|listing|subscription|membership|billing)\b/.test(haystack)) {
1218
1227
  return "Customer";
1219
1228
  }
1220
1229
  if (hasUserFacingEntrypointOrFile(flow)) {
@@ -1929,6 +1938,27 @@ export function formatMarkdownE2eSetup(result) {
1929
1938
  }
1930
1939
  return lines.join("\n");
1931
1940
  }
1941
+ const frameworkSignalDependencies = [
1942
+ "expo",
1943
+ "react-native",
1944
+ "@playwright/test",
1945
+ "playwright",
1946
+ "@angular/core",
1947
+ "@remix-run/react",
1948
+ "astro",
1949
+ "next",
1950
+ "nuxt",
1951
+ "react-dom",
1952
+ "react-router-dom",
1953
+ "svelte",
1954
+ "vue",
1955
+ "vite",
1956
+ "@nestjs/core",
1957
+ "express",
1958
+ "fastify",
1959
+ "koa",
1960
+ "hono",
1961
+ ];
1932
1962
  async function detectProjectProfile(root, workspaceRoot) {
1933
1963
  const profileRoots = profileSearchRoots(root, workspaceRoot);
1934
1964
  const packageJson = await readPackageJson(root);
@@ -1940,6 +1970,20 @@ async function detectProjectProfile(root, workspaceRoot) {
1940
1970
  ...(packageJson?.devDependencies ?? {}),
1941
1971
  };
1942
1972
  const evidence = [];
1973
+ const rootHasFrameworkSignal = frameworkSignalDependencies.some((dependency) => dependency in dependencies);
1974
+ if (!rootHasFrameworkSignal) {
1975
+ const memberDependencies = await collectWorkspaceMemberDependencies(root, packageJson);
1976
+ for (const member of memberDependencies) {
1977
+ for (const [dependency, version] of Object.entries(member.dependencies)) {
1978
+ if (!(dependency in dependencies)) {
1979
+ dependencies[dependency] = version;
1980
+ }
1981
+ }
1982
+ if (member.frameworkSignals.length > 0) {
1983
+ evidence.push(`workspace member ${member.directory}: ${member.frameworkSignals.join(", ")}`);
1984
+ }
1985
+ }
1986
+ }
1943
1987
  const hasExpoDependency = "expo" in dependencies;
1944
1988
  const hasReactNativeDependency = "react-native" in dependencies;
1945
1989
  const hasPlaywrightDependency = "@playwright/test" in dependencies || "playwright" in dependencies;
@@ -2942,21 +2986,64 @@ function toCoreFlowChangedFiles(changedFiles, scopedRoot, coreFlowRoot) {
2942
2986
  previousPath: file.previousPath ? toPosixPath(path.join(relativeRoot, file.previousPath)) : undefined,
2943
2987
  }));
2944
2988
  }
2945
- async function buildFlows(root, changedFiles, runner, projectType, testSuiteInventory, domainLanguage) {
2989
+ async function buildFlows(root, changedFiles, runner, projectType, testSuiteInventory, domainLanguage, addedDiffText = {}) {
2946
2990
  const files = changedFiles.map((file) => file.path);
2991
+ const importImpacts = await collectImportImpacts(root, files);
2947
2992
  const fixtureContext = await collectFixtureReadinessContext(root, files);
2948
- const flowResults = await Promise.all(buildFlowCandidates(files, runner, projectType, domainLanguage).map((candidate) => buildFlow(root, runner, candidate, testSuiteInventory, fixtureContext)));
2993
+ const flowResults = await Promise.all(buildFlowCandidates(files, runner, projectType, domainLanguage, importImpacts).map((candidate) => buildFlow(root, runner, candidate, testSuiteInventory, fixtureContext, addedDiffText)));
2949
2994
  const flows = flowResults.filter((flow) => Boolean(flow));
2950
2995
  return dedupeFlows(flows).slice(0, 4);
2951
2996
  }
2952
- function buildFlowCandidates(files, runner, projectType, domainLanguage) {
2953
- const lowSignalCandidate = buildLowSignalChangeCandidate(files);
2997
+ async function expandChangedFilesForMatching(root, changedFiles) {
2998
+ if (changedFiles.length === 0 || changedFiles.length > 60) {
2999
+ return changedFiles;
3000
+ }
3001
+ try {
3002
+ const expansion = await expandChangedFilesWithImporters(root, changedFiles.map((file) => file.path));
3003
+ const knownPaths = new Set(changedFiles.map((file) => file.path));
3004
+ const importerEntries = expansion.files
3005
+ .filter((file) => !knownPaths.has(file))
3006
+ .map((file) => ({ status: "M", path: file }));
3007
+ return [...changedFiles, ...importerEntries];
3008
+ }
3009
+ catch {
3010
+ return changedFiles;
3011
+ }
3012
+ }
3013
+ function isRoutableSurfaceFile(file) {
3014
+ if (isApiRouteFile(file) || isTestLikeFile(file)) {
3015
+ return false;
3016
+ }
3017
+ return (/(?:^|\/)app\/.*(?:^|\/)?page\.[cm]?[jt]sx?$/i.test(file) ||
3018
+ /(?:^|\/)pages\/(?!api\/).+\.(?:[cm]?[jt]sx?|vue|svelte)$/i.test(file) ||
3019
+ /(?:^|\/)(?:screens|views)\/.+\.(?:[cm]?[jt]sx?|vue|svelte)$/i.test(file) ||
3020
+ /(?:^|\/)routes\/(?!api\/).+\.(?:[cm]?[jt]sx?|vue|svelte)$/i.test(file));
3021
+ }
3022
+ async function collectImportImpacts(root, changedFiles) {
3023
+ const propagatableFiles = changedFiles.filter((file) => !isRoutableSurfaceFile(file) && !isTestLikeFile(file) && !isConfigLikeFile(file) && !isContentOrStyleFile(file));
3024
+ if (propagatableFiles.length === 0) {
3025
+ return [];
3026
+ }
3027
+ try {
3028
+ const index = await buildReverseImportIndex(root);
3029
+ return findImportingSurfaces(index, propagatableFiles, isRoutableSurfaceFile);
3030
+ }
3031
+ catch {
3032
+ return [];
3033
+ }
3034
+ }
3035
+ function describeImportChain(impact) {
3036
+ return impact.chain.join(" -> ");
3037
+ }
3038
+ function buildFlowCandidates(files, runner, projectType, domainLanguage, importImpacts = []) {
3039
+ const lowSignalCandidate = importImpacts.length === 0 ? buildLowSignalChangeCandidate(files) : undefined;
2954
3040
  if (lowSignalCandidate) {
2955
3041
  return [lowSignalCandidate];
2956
3042
  }
2957
3043
  const behaviorFiles = files.filter((file) => !isTestLikeFile(file));
2958
3044
  const candidateFiles = behaviorFiles.length > 0 ? behaviorFiles : files;
2959
- const uiFiles = candidateFiles.filter(isUserFacingFile);
3045
+ const impactSurfaceFiles = importImpacts.map((impact) => impact.surface).filter((surface) => !candidateFiles.includes(surface));
3046
+ const uiFiles = uniqueStrings([...candidateFiles.filter(isUserFacingFile), ...impactSurfaceFiles]);
2960
3047
  const apiFiles = candidateFiles.filter(isApiLikeFile);
2961
3048
  const apiServiceSourceFiles = projectType === "api-service"
2962
3049
  ? candidateFiles.filter((file) => !apiFiles.includes(file) && !isConfigLikeFile(file) && isServiceSourceFile(file))
@@ -2975,11 +3062,14 @@ function buildFlowCandidates(files, runner, projectType, domainLanguage) {
2975
3062
  const candidates = [];
2976
3063
  if (uiFiles.length > 0) {
2977
3064
  const subject = summarizeFlowSubject(uiFiles, "Changed", domainLanguage);
3065
+ const impactReason = importImpacts.length > 0
3066
+ ? ` Changed shared files reach these surfaces through imports: ${importImpacts.slice(0, 3).map(describeImportChain).join("; ")}.`
3067
+ : "";
2978
3068
  candidates.push({
2979
3069
  kind: "ui",
2980
3070
  title: `${subject} UI smoke flow`,
2981
- reason: "User-facing route, screen, navigation, or component files changed, so the draft should open the touched surface and cover the primary visible action.",
2982
- files: uiFiles,
3071
+ reason: `User-facing route, screen, navigation, or component files changed, so the draft should open the touched surface and cover the primary visible action.${impactReason}`,
3072
+ files: uniqueStrings([...uiFiles, ...importImpacts.map((impact) => impact.changedFile)]),
2983
3073
  steps: [
2984
3074
  "Launch the app.",
2985
3075
  "Navigate to the changed screen or component surface.",
@@ -3212,14 +3302,14 @@ function buildLowSignalChangeCandidate(files) {
3212
3302
  }
3213
3303
  return undefined;
3214
3304
  }
3215
- async function buildFlow(root, runner, candidate, testSuiteInventory, fixtureContext) {
3305
+ async function buildFlow(root, runner, candidate, testSuiteInventory, fixtureContext, addedDiffText = {}) {
3216
3306
  const files = uniqueStrings(candidate.files).slice(0, 20);
3217
3307
  if (files.length === 0) {
3218
3308
  return undefined;
3219
3309
  }
3220
3310
  const coverage = buildCoverageTargets(candidate.kind, files, runner);
3221
3311
  const setupHints = await inferFlowSetupHints(root, files, candidate.kind);
3222
- const selectors = await inferFlowSelectors(root, files, runner);
3312
+ const selectors = await inferFlowSelectors(root, files, runner, addedDiffText);
3223
3313
  const flow = {
3224
3314
  title: candidate.title,
3225
3315
  reason: candidate.reason,
@@ -3238,9 +3328,12 @@ async function buildFlow(root, runner, candidate, testSuiteInventory, fixtureCon
3238
3328
  languageBrief: buildFlowLanguageBrief(flow),
3239
3329
  };
3240
3330
  }
3331
+ function preferDiffAdded(selectors, predicate) {
3332
+ return selectors.find((selector) => selector.addedInDiff && predicate(selector)) ?? selectors.find(predicate);
3333
+ }
3241
3334
  function refineStepsForInferredSelectors(steps, selectors) {
3242
- const inputSelector = selectors.find(isInputSelector);
3243
- const actionSelector = selectors.find((selector) => selectorCanDriveInteraction(selector) && !isInputSelector(selector));
3335
+ const inputSelector = preferDiffAdded(selectors, isInputSelector);
3336
+ const actionSelector = preferDiffAdded(selectors, (selector) => selectorCanDriveInteraction(selector) && !isInputSelector(selector));
3244
3337
  if (!inputSelector || !actionSelector || steps.some((step) => /^\s*(?:fill|input|enter|type|provide|write)\b/i.test(step))) {
3245
3338
  return steps;
3246
3339
  }
@@ -3257,8 +3350,8 @@ function refineStepsForInferredSelectors(steps, selectors) {
3257
3350
  return uniqueStrings(refined);
3258
3351
  }
3259
3352
  function refineManifestStepsForInferredSelectors(steps, selectors) {
3260
- const inputSelector = selectors.find(isInputSelector);
3261
- const actionSelector = selectors.find((selector) => selectorCanDriveInteraction(selector) && !isInputSelector(selector));
3353
+ const inputSelector = preferDiffAdded(selectors, isInputSelector);
3354
+ const actionSelector = preferDiffAdded(selectors, (selector) => selectorCanDriveInteraction(selector) && !isInputSelector(selector));
3262
3355
  if (!inputSelector || !actionSelector || steps.some(isInputStep)) {
3263
3356
  return steps;
3264
3357
  }
@@ -3280,7 +3373,12 @@ function exerciseStepSubject(step) {
3280
3373
  return undefined;
3281
3374
  }
3282
3375
  function selectorStepLabel(selector) {
3283
- return titleCase(selector.value.replace(/[-_]+/g, " "));
3376
+ const label = titleCase(selector.value.replace(/[-_]+/g, " "));
3377
+ if (label) {
3378
+ return label;
3379
+ }
3380
+ const raw = selector.value.trim();
3381
+ return raw.length > 0 ? `"${raw}"` : `the ${selector.kind} control`;
3284
3382
  }
3285
3383
  function actionVerbForSelector(selector) {
3286
3384
  if (/\b(?:submit|send|apply|complete|confirm|save|continue|next|upload)\b/i.test(selector.value.replace(/[-_]+/g, " "))) {
@@ -3588,7 +3686,8 @@ function routeSegmentsFromFileParts(parts) {
3588
3686
  function isBackendImplementationFile(file) {
3589
3687
  return /(?:^|\/)(?:server|servers|backend|api|apis|routes|controllers?|handlers?|resolvers?|endpoints?)\//i.test(file) ||
3590
3688
  /(?:openapi|swagger|schema|controller|handler|resolver|route)\.(?:json|ya?ml|[cm]?[jt]sx?|py|go|rs|kt|java|rb|php)$/i.test(file) ||
3591
- /(?:^|\/)(?:urls|views|serializers|routers|controllers?|handlers?|admin|models|services|tasks|permissions|filters)\.py$/i.test(file);
3689
+ /(?:^|\/)(?:urls|views|viewsets|serializers|routers|controllers?|handlers?|admin|models|services|tasks|permissions|filters|consumers|signals)\w*\.py$/i.test(file) ||
3690
+ /(?:^|\/)(?:views|viewsets|serializers|services|routers|handlers|tasks|consumers)\/[^/]+\.py$/i.test(file);
3592
3691
  }
3593
3692
  function isMockOrFixtureFile(file) {
3594
3693
  if (isFixtureEvidenceIgnoredPath(file)) {
@@ -3665,7 +3764,7 @@ const fixtureEvidenceIgnoredTokens = new Set([
3665
3764
  "test",
3666
3765
  "tests",
3667
3766
  ]);
3668
- async function inferFlowSelectors(root, files, runner) {
3767
+ async function inferFlowSelectors(root, files, runner, addedDiffText = {}) {
3669
3768
  const selectors = [];
3670
3769
  for (const file of files.slice(0, 8)) {
3671
3770
  if (!isUiImplementationFile(file)) {
@@ -3675,7 +3774,10 @@ async function inferFlowSelectors(root, files, runner) {
3675
3774
  if (!text) {
3676
3775
  continue;
3677
3776
  }
3678
- selectors.push(...extractSelectorsFromText(file, text, runner));
3777
+ const addedText = addedDiffText[file];
3778
+ for (const selector of extractSelectorsFromText(file, text, runner)) {
3779
+ selectors.push(addedText && addedText.includes(selector.value) ? { ...selector, addedInDiff: true } : selector);
3780
+ }
3679
3781
  }
3680
3782
  return uniqueSelectors(selectors).slice(0, 12);
3681
3783
  }
@@ -4085,7 +4187,8 @@ function isUiImplementationFile(file) {
4085
4187
  }
4086
4188
  function isServiceSourceFile(file) {
4087
4189
  return /(?:^|\/)src\/.+\.(?:[cm]?[jt]s|py|go|rs|java|kt|cs)$/i.test(file) ||
4088
- /(?:^|\/)(?:urls|views|serializers|routers|controllers?|handlers?|admin|models|services|tasks|permissions|filters)\.py$/i.test(file);
4190
+ /(?:^|\/)(?:urls|views|viewsets|serializers|routers|controllers?|handlers?|admin|models|services|tasks|permissions|filters|consumers|signals)\w*\.py$/i.test(file) ||
4191
+ /(?:^|\/)(?:views|viewsets|serializers|services|api|apis|routers|handlers|tasks|consumers)\/[^/]+\.py$/i.test(file);
4089
4192
  }
4090
4193
  function summarizeFlowSubject(files, fallback, domainLanguage) {
4091
4194
  const languageSubject = summarizeFlowSubjectFromDomainLanguage(files, domainLanguage);
@@ -4338,7 +4441,7 @@ function titleCase(value) {
4338
4441
  return value
4339
4442
  .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
4340
4443
  .replace(/[-_]+/g, " ")
4341
- .replace(/[^a-zA-Z0-9 ]+/g, " ")
4444
+ .replace(/[^\p{L}\p{N} ]+/gu, " ")
4342
4445
  .trim()
4343
4446
  .split(/\s+/)
4344
4447
  .filter(Boolean)
@@ -6248,42 +6351,56 @@ function formatMaestroCommand(command) {
6248
6351
  }
6249
6352
  return [`- ${command.kind}: ${command.value}`];
6250
6353
  }
6354
+ function takePreferredSelector(selectors, predicate, diffGate = predicate) {
6355
+ let firstIndex = -1;
6356
+ for (let index = 0; index < selectors.length; index += 1) {
6357
+ const selector = selectors[index];
6358
+ if (!predicate(selector)) {
6359
+ continue;
6360
+ }
6361
+ if (selector.addedInDiff && diffGate(selector)) {
6362
+ return selectors.splice(index, 1)[0];
6363
+ }
6364
+ if (firstIndex === -1) {
6365
+ firstIndex = index;
6366
+ }
6367
+ }
6368
+ return firstIndex >= 0 ? selectors.splice(firstIndex, 1)[0] : undefined;
6369
+ }
6251
6370
  function takeSelectorForStep(selectors, step) {
6252
6371
  if (/^launch\b/i.test(step)) {
6253
6372
  return undefined;
6254
6373
  }
6255
6374
  if (isInputStep(step)) {
6256
- const inputIndex = selectors.findIndex((selector) => isInputSelector(selector) && selectorMatchesStep(selector, step));
6257
- if (inputIndex >= 0) {
6258
- const [selector] = selectors.splice(inputIndex, 1);
6259
- return selector;
6375
+ const matched = takePreferredSelector(selectors, (selector) => isInputSelector(selector) && selectorMatchesStep(selector, step));
6376
+ if (matched) {
6377
+ return matched;
6260
6378
  }
6261
- const fallbackInputIndex = selectors.findIndex(isInputSelector);
6262
- if (fallbackInputIndex >= 0) {
6263
- const [selector] = selectors.splice(fallbackInputIndex, 1);
6264
- return selector;
6379
+ const fallbackInput = takePreferredSelector(selectors, isInputSelector);
6380
+ if (fallbackInput) {
6381
+ return fallbackInput;
6265
6382
  }
6266
6383
  }
6267
6384
  if (!isInteractionStep(step) && !isVerificationStep(step) && !canUsePrimarySelector(step)) {
6268
6385
  return undefined;
6269
6386
  }
6270
- const index = selectors.findIndex((selector) => !isInputSelector(selector) && selectorMatchesStep(selector, step));
6271
- if (index >= 0) {
6272
- const [selector] = selectors.splice(index, 1);
6273
- return selector;
6387
+ const diffGateForStep = isAssertionStep(step) || isVerificationStep(step)
6388
+ ? selectorCanSupportAssertion
6389
+ : selectorCanDriveInteraction;
6390
+ const matched = takePreferredSelector(selectors, (selector) => !isInputSelector(selector) && selectorMatchesStep(selector, step), diffGateForStep);
6391
+ if (matched) {
6392
+ return matched;
6274
6393
  }
6275
6394
  if (isAssertionStep(step)) {
6276
- const assertionIndex = selectors.findIndex((selector) => selectorCanSupportAssertion(selector));
6277
- if (assertionIndex >= 0) {
6278
- const [selector] = selectors.splice(assertionIndex, 1);
6279
- return selector;
6395
+ const assertion = takePreferredSelector(selectors, selectorCanSupportAssertion);
6396
+ if (assertion) {
6397
+ return assertion;
6280
6398
  }
6281
6399
  }
6282
6400
  if (canUsePrimarySelector(step)) {
6283
- const fallbackIndex = selectors.findIndex((selector) => selectorCanDriveInteraction(selector));
6284
- if (fallbackIndex >= 0) {
6285
- const [selector] = selectors.splice(fallbackIndex, 1);
6286
- return selector;
6401
+ const fallback = takePreferredSelector(selectors, selectorCanDriveInteraction);
6402
+ if (fallback) {
6403
+ return fallback;
6287
6404
  }
6288
6405
  }
6289
6406
  return undefined;
@@ -6654,6 +6771,77 @@ async function readPackageJson(root) {
6654
6771
  return undefined;
6655
6772
  }
6656
6773
  }
6774
+ const maxWorkspaceMembers = 30;
6775
+ async function collectWorkspaceMemberDependencies(root, packageJson) {
6776
+ const patterns = await readWorkspaceMemberPatterns(root, packageJson);
6777
+ if (patterns.length === 0) {
6778
+ return [];
6779
+ }
6780
+ const memberDirectories = await expandWorkspaceMemberPatterns(root, patterns);
6781
+ const members = [];
6782
+ for (const directory of memberDirectories.slice(0, maxWorkspaceMembers)) {
6783
+ const memberPackageJson = await readPackageJson(path.join(root, directory));
6784
+ if (!memberPackageJson) {
6785
+ continue;
6786
+ }
6787
+ const dependencies = {
6788
+ ...(memberPackageJson.dependencies ?? {}),
6789
+ ...(memberPackageJson.devDependencies ?? {}),
6790
+ };
6791
+ members.push({
6792
+ directory,
6793
+ dependencies,
6794
+ frameworkSignals: frameworkSignalDependencies.filter((dependency) => dependency in dependencies),
6795
+ });
6796
+ }
6797
+ return members;
6798
+ }
6799
+ async function readWorkspaceMemberPatterns(root, packageJson) {
6800
+ const patterns = [];
6801
+ const workspaces = packageJson?.workspaces;
6802
+ if (Array.isArray(workspaces)) {
6803
+ patterns.push(...workspaces);
6804
+ }
6805
+ else if (workspaces?.packages) {
6806
+ patterns.push(...workspaces.packages);
6807
+ }
6808
+ const workspaceYaml = await readTextIfExists(path.join(root, "pnpm-workspace.yaml"));
6809
+ if (workspaceYaml) {
6810
+ try {
6811
+ const parsed = YAML.parse(workspaceYaml);
6812
+ patterns.push(...(parsed?.packages ?? []));
6813
+ }
6814
+ catch {
6815
+ // ignore unparseable workspace config
6816
+ }
6817
+ }
6818
+ return uniqueStrings(patterns.filter((pattern) => typeof pattern === "string" && !pattern.startsWith("!")));
6819
+ }
6820
+ async function expandWorkspaceMemberPatterns(root, patterns) {
6821
+ const directories = [];
6822
+ for (const pattern of patterns) {
6823
+ const normalized = pattern.replace(/\/\*{1,2}$/, "");
6824
+ if (normalized.includes("*")) {
6825
+ continue;
6826
+ }
6827
+ if (normalized === pattern) {
6828
+ directories.push(normalized);
6829
+ continue;
6830
+ }
6831
+ try {
6832
+ const entries = await fs.readdir(path.join(root, normalized), { withFileTypes: true });
6833
+ for (const entry of entries) {
6834
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
6835
+ directories.push(`${normalized}/${entry.name}`);
6836
+ }
6837
+ }
6838
+ }
6839
+ catch {
6840
+ continue;
6841
+ }
6842
+ }
6843
+ return uniqueStrings(directories);
6844
+ }
6657
6845
  async function readTextIfExists(filePath) {
6658
6846
  try {
6659
6847
  return await fs.readFile(filePath, "utf8");