@skyramp/mcp 0.3.1 → 0.3.2-rc.pom-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/build/index.js +2 -1
- package/build/prompts/code-reuse.d.ts +7 -1
- package/build/prompts/code-reuse.js +8 -5
- package/build/prompts/code-reuse.test.d.ts +1 -0
- package/build/prompts/code-reuse.test.js +62 -0
- package/build/prompts/pom-aware-code-reuse.d.ts +6 -1
- package/build/prompts/pom-aware-code-reuse.js +100 -53
- package/build/prompts/pom-aware-code-reuse.test.d.ts +1 -0
- package/build/prompts/pom-aware-code-reuse.test.js +11 -0
- package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +4 -2
- package/build/prompts/test-recommendation/diffExecutionPlan.js +11 -65
- package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +2 -2
- package/build/prompts/test-recommendation/recommendationSections.js +5 -2
- package/build/prompts/test-recommendation/scopeAssessment.js +1 -1
- package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +26 -1
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +68 -56
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +48 -11
- package/build/prompts/testbot/testbot-prompts.d.ts +1 -1
- package/build/prompts/testbot/testbot-prompts.js +67 -21
- package/build/prompts/testbot/testbot-prompts.test.js +44 -0
- package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +7 -0
- package/build/recommendation/budgeters/diversityBalancedBudgeter.js +71 -0
- package/build/recommendation/budgeters/diversityBalancedBudgeter.test.d.ts +1 -0
- package/build/recommendation/budgeters/diversityBalancedBudgeter.test.js +75 -0
- package/build/recommendation/budgeters/fixedNBudgeter.d.ts +7 -0
- package/build/recommendation/budgeters/fixedNBudgeter.js +11 -0
- package/build/recommendation/budgeters/fixedNBudgeter.test.d.ts +1 -0
- package/build/recommendation/budgeters/fixedNBudgeter.test.js +66 -0
- package/build/recommendation/budgeters/shared.d.ts +19 -0
- package/build/recommendation/budgeters/shared.js +66 -0
- package/build/recommendation/discriminators.d.ts +31 -0
- package/build/recommendation/discriminators.js +355 -0
- package/build/recommendation/discriminators.test.d.ts +1 -0
- package/build/recommendation/discriminators.test.js +324 -0
- package/build/recommendation/diversity.d.ts +47 -0
- package/build/recommendation/diversity.js +101 -0
- package/build/recommendation/diversity.test.d.ts +1 -0
- package/build/recommendation/diversity.test.js +77 -0
- package/build/recommendation/planRanker.d.ts +50 -0
- package/build/recommendation/planRanker.js +67 -0
- package/build/recommendation/planRanker.test.d.ts +1 -0
- package/build/recommendation/planRanker.test.js +110 -0
- package/build/recommendation/testFixtures.d.ts +25 -0
- package/build/recommendation/testFixtures.js +45 -0
- package/build/resources/testbotResource.js +4 -1
- package/build/services/ScenarioGenerationService.d.ts +5 -0
- package/build/services/ScenarioGenerationService.js +16 -1
- package/build/services/ScenarioGenerationService.test.js +44 -0
- package/build/services/TestExecutionService.d.ts +15 -1
- package/build/services/TestExecutionService.js +210 -55
- package/build/services/TestExecutionService.test.js +397 -0
- package/build/services/TestGenerationService.js +19 -1
- package/build/services/TestGenerationService.test.js +58 -0
- package/build/tool-phases.js +1 -0
- package/build/toolNames.d.ts +19 -0
- package/build/toolNames.js +19 -0
- package/build/tools/code-refactor/codeReuseTool.d.ts +7 -0
- package/build/tools/code-refactor/codeReuseTool.js +130 -4
- package/build/tools/code-refactor/codeReuseTool.test.d.ts +1 -0
- package/build/tools/code-refactor/codeReuseTool.test.js +290 -0
- package/build/tools/executeSkyrampTestTool.js +8 -2
- package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +6 -1
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +110 -17
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +147 -0
- package/build/tools/generate-tests/generateContractRestTool.js +11 -1
- package/build/tools/generate-tests/generateIntegrationRestTool.js +24 -1
- package/build/tools/generate-tests/generateIntegrationRestTool.test.d.ts +1 -0
- package/build/tools/generate-tests/generateIntegrationRestTool.test.js +159 -0
- package/build/tools/generate-tests/planGuard.d.ts +13 -0
- package/build/tools/generate-tests/planGuard.js +78 -0
- package/build/tools/generate-tests/planGuard.test.d.ts +1 -0
- package/build/tools/generate-tests/planGuard.test.js +185 -0
- package/build/tools/generate-tests/scenarioFileIdentity.d.ts +10 -0
- package/build/tools/generate-tests/scenarioFileIdentity.js +46 -0
- package/build/tools/generate-tests/scenarioLint.d.ts +30 -0
- package/build/tools/generate-tests/scenarioLint.js +150 -0
- package/build/tools/generate-tests/scenarioLint.test.d.ts +1 -0
- package/build/tools/generate-tests/scenarioLint.test.js +100 -0
- package/build/tools/submitReportTool.js +78 -0
- package/build/tools/submitReportTool.test.js +255 -0
- package/build/tools/test-management/analyzeChangesTool.js +55 -2
- package/build/tools/test-management/analyzeChangesTool.test.js +12 -0
- package/build/tools/test-management/index.d.ts +1 -0
- package/build/tools/test-management/index.js +1 -0
- package/build/tools/test-management/registerTestPlanTool.d.ts +2 -0
- package/build/tools/test-management/registerTestPlanTool.js +329 -0
- package/build/tools/test-management/registerTestPlanTool.test.d.ts +1 -0
- package/build/tools/test-management/registerTestPlanTool.test.js +296 -0
- package/build/types/Recommendation.d.ts +97 -0
- package/build/types/Recommendation.js +48 -0
- package/build/types/RepositoryAnalysis.d.ts +14 -14
- package/build/types/TestExecution.d.ts +2 -0
- package/build/types/TestRecommendation.d.ts +12 -1
- package/build/types/TestRecommendation.js +26 -11
- package/build/types/TestTypes.js +1 -1
- package/build/utils/AnalysisStateManager.d.ts +47 -0
- package/build/utils/docker.test.js +1 -1
- package/build/utils/planMatchKeys.d.ts +61 -0
- package/build/utils/planMatchKeys.js +125 -0
- package/build/utils/pom-scope/import-expansion.d.ts +5 -0
- package/build/utils/pom-scope/import-expansion.js +32 -0
- package/build/utils/pom-scope/index.d.ts +39 -0
- package/build/utils/pom-scope/index.js +120 -0
- package/build/utils/pom-scope/index.test.d.ts +1 -0
- package/build/utils/pom-scope/index.test.js +239 -0
- package/build/utils/pom-scope/pom-files.d.ts +3 -0
- package/build/utils/pom-scope/pom-files.js +48 -0
- package/build/utils/pom-scope/pom-files.test.d.ts +1 -0
- package/build/utils/pom-scope/pom-files.test.js +29 -0
- package/build/utils/pom-scope/scoring.d.ts +20 -0
- package/build/utils/pom-scope/scoring.js +45 -0
- package/build/utils/pom-scope/scoring.test.d.ts +1 -0
- package/build/utils/pom-scope/scoring.test.js +39 -0
- package/build/utils/pom-scope/selector-extractor.d.ts +7 -0
- package/build/utils/pom-scope/selector-extractor.js +57 -0
- package/build/utils/pom-scope/selector-extractor.test.d.ts +1 -0
- package/build/utils/pom-scope/selector-extractor.test.js +67 -0
- package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.d.ts +5 -0
- package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.js +5 -0
- package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.d.ts +5 -0
- package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.js +9 -0
- package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.d.ts +4 -0
- package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.js +4 -0
- package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.d.ts +4 -0
- package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.js +4 -0
- package/build/utils/pom-verify/bindings.d.ts +19 -0
- package/build/utils/pom-verify/bindings.js +161 -0
- package/build/utils/pom-verify/bindings.test.d.ts +1 -0
- package/build/utils/pom-verify/bindings.test.js +164 -0
- package/build/utils/pom-verify/calls.d.ts +16 -0
- package/build/utils/pom-verify/calls.js +42 -0
- package/build/utils/pom-verify/calls.test.d.ts +1 -0
- package/build/utils/pom-verify/calls.test.js +61 -0
- package/build/utils/pom-verify/index.d.ts +4 -0
- package/build/utils/pom-verify/index.js +4 -0
- package/build/utils/pom-verify/resolve.d.ts +7 -0
- package/build/utils/pom-verify/resolve.js +27 -0
- package/build/utils/pom-verify/resolve.test.d.ts +1 -0
- package/build/utils/pom-verify/resolve.test.js +68 -0
- package/build/utils/pom-verify/strip.d.ts +9 -0
- package/build/utils/pom-verify/strip.js +89 -0
- package/build/utils/pom-verify/verify.d.ts +14 -0
- package/build/utils/pom-verify/verify.js +158 -0
- package/build/utils/pom-verify/verify.test.d.ts +1 -0
- package/build/utils/pom-verify/verify.test.js +325 -0
- package/build/utils/reportVerification.d.ts +61 -0
- package/build/utils/reportVerification.js +104 -0
- package/build/utils/reportVerification.test.d.ts +1 -0
- package/build/utils/reportVerification.test.js +185 -0
- package/build/utils/scenarioDrafting.js +5 -5
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/utils/workspaceAuth.d.ts +9 -1
- package/build/utils/workspaceAuth.js +25 -5
- package/build/utils/workspaceAuth.test.js +48 -0
- package/build/workspace/workspace.d.ts +20 -0
- package/build/workspace/workspace.js +4 -0
- package/build/workspace/workspace.test.js +10 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +77 -8
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/volatileDate.js +101 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/utils.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-aszq5EdG.js → codeMirrorModule-Bzd72-bG.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-BxS7Jm4s.js → defaultSettingsView-DzxTioTK.js} +101 -101
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.D4JTTy4R.js → index.BGc30U3S.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.DaRMQKOI.js → uiMode.IaDrb29A.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/utils/isomorphic/volatileDate.ts +131 -0
- package/node_modules/playwright/node_modules/playwright-core/src/utils.ts +1 -0
- package/node_modules/playwright/package.json +1 -1
- package/package.json +3 -3
- package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts selector *value tokens* from a generated test's source. Tokens are
|
|
3
|
+
* the join key between the raw spec and the POM layer: we grep POM files for
|
|
4
|
+
* these strings to find which page objects cover the test's actions.
|
|
5
|
+
*/
|
|
6
|
+
// Default test-id attribute family. Playwright's own default is data-testid, but
|
|
7
|
+
// data-test/data-qa/data-cy are common conventions in the wild — match them all.
|
|
8
|
+
export const DEFAULT_TESTID_ATTRS = ["data-test", "data-testid", "data-qa", "data-cy"];
|
|
9
|
+
// Full regex-escape for attr names — a config-supplied testIdAttribute could contain
|
|
10
|
+
// a stray metachar, which must not be allowed to distort the pattern.
|
|
11
|
+
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
12
|
+
// [data-test="X"] incl. prefix/suffix matchers; allows embedded escapes (\\/)
|
|
13
|
+
// but a backslash pair may not consume a quote, so the escaped closing \" terminates
|
|
14
|
+
const attrPattern = (attrs) => new RegExp(`\\[(?:${attrs.map(esc).join("|")})[$^*~|]?=\\\\?["']((?:\\\\[^"']|[^"'\\\\\\]])+)`, "g");
|
|
15
|
+
// Attribute-shaped anchor (no captured value) used to detect whether a raw locator
|
|
16
|
+
// string is actually a family-attr selector, vs. one that merely CONTAINS the
|
|
17
|
+
// substring (e.g. ".data-testing-notes" contains "data-test" but isn't one).
|
|
18
|
+
const attrAnchor = (attrs) => new RegExp(`\\[(?:${attrs.map(esc).join("|")})[$^*~|]?=`);
|
|
19
|
+
const OTHER_PATTERNS = [
|
|
20
|
+
/getByTestId\(\s*["']([^"']+)["']/g, // getByTestId("X")
|
|
21
|
+
/getByRole\(\s*["']([^"']+)["']/g, // getByRole("role", ...)
|
|
22
|
+
/name:\s*["']([^"']+)["']/g, // { name: "Accessible Name" }
|
|
23
|
+
/getBy(?:Text|Label|Placeholder)\(\s*["']([^"']+)["']/g, // getByText("T") etc.
|
|
24
|
+
];
|
|
25
|
+
// Raw .locator("...")/.frameLocator("...") strings that are NOT test-id-attribute based
|
|
26
|
+
// (e.g. ".step-deduplication", "iframe#asset-list").
|
|
27
|
+
// Quote-type-aware alternates: a double-quoted arg may contain unescaped single quotes
|
|
28
|
+
// (and vice versa) — the common has-text('...') / has-text("...") idiom nests the
|
|
29
|
+
// other quote character. Each branch allows escapes (\\.) or any char but its own
|
|
30
|
+
// quote/backslash.
|
|
31
|
+
const RAW_LOCATOR = /\.(?:locator|frameLocator)\(\s*(?:"((?:\\.|[^"\\])+)"|'((?:\\.|[^'\\])+)')\s*\)/g;
|
|
32
|
+
function normalize(token) {
|
|
33
|
+
return token.replace(/\\/g, "");
|
|
34
|
+
}
|
|
35
|
+
function isUseful(token) {
|
|
36
|
+
return token.length >= 3 && !/^\d+$/.test(token);
|
|
37
|
+
}
|
|
38
|
+
export function extractSelectors(specContent, extraTestIdAttrs = []) {
|
|
39
|
+
const testIdAttrs = [...DEFAULT_TESTID_ATTRS, ...extraTestIdAttrs];
|
|
40
|
+
const anchor = attrAnchor(testIdAttrs);
|
|
41
|
+
const tokens = new Set();
|
|
42
|
+
for (const re of [attrPattern(testIdAttrs), ...OTHER_PATTERNS]) {
|
|
43
|
+
for (const m of specContent.matchAll(re)) {
|
|
44
|
+
const t = normalize(m[1]);
|
|
45
|
+
if (isUseful(t))
|
|
46
|
+
tokens.add(t);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const m of specContent.matchAll(RAW_LOCATOR)) {
|
|
50
|
+
const raw = normalize(m[1] ?? m[2]);
|
|
51
|
+
if (anchor.test(raw))
|
|
52
|
+
continue; // value already captured above
|
|
53
|
+
if (isUseful(raw))
|
|
54
|
+
tokens.add(raw);
|
|
55
|
+
}
|
|
56
|
+
return [...tokens];
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { extractSelectors } from "./selector-extractor.js";
|
|
2
|
+
describe("extractSelectors", () => {
|
|
3
|
+
it("extracts data-test attribute values", () => {
|
|
4
|
+
const spec = `await page.locator("[data-test=\\"login-sign-in\\"]").click();
|
|
5
|
+
await page.locator('[data-test="add-template-name-input-field"]').fill("x");`;
|
|
6
|
+
const t = extractSelectors(spec);
|
|
7
|
+
expect(t).toContain("login-sign-in");
|
|
8
|
+
expect(t).toContain("add-template-name-input-field");
|
|
9
|
+
});
|
|
10
|
+
it("extracts getByTestId / getByRole / getByText tokens", () => {
|
|
11
|
+
const spec = `page.getByTestId("tpl-add");
|
|
12
|
+
page.getByRole("button", { name: "Save Template" });
|
|
13
|
+
page.getByText("Templates");`;
|
|
14
|
+
const t = extractSelectors(spec);
|
|
15
|
+
expect(t).toEqual(expect.arrayContaining(["tpl-add", "button", "Save Template", "Templates"]));
|
|
16
|
+
});
|
|
17
|
+
it("normalizes escaped slashes and dedupes", () => {
|
|
18
|
+
const spec = `page.locator("[data-test=\\"menu-link-\\\\/alerts-item\\"]");
|
|
19
|
+
page.locator('[data-test="menu-link-/alerts-item"]');`;
|
|
20
|
+
const t = extractSelectors(spec);
|
|
21
|
+
expect(t.filter((x) => x === "menu-link-/alerts-item")).toHaveLength(1);
|
|
22
|
+
expect(t).not.toContain("menu-link-");
|
|
23
|
+
});
|
|
24
|
+
it("extracts values from prefix/suffix data-test matchers", () => {
|
|
25
|
+
const spec = `page.locator('[data-test^="ai-integrations-item-"]'); page.locator('[data-test*="row-cell"]');`;
|
|
26
|
+
const t = extractSelectors(spec);
|
|
27
|
+
expect(t).toEqual(expect.arrayContaining(["ai-integrations-item-", "row-cell"]));
|
|
28
|
+
});
|
|
29
|
+
it("keeps non-data-test raw locator strings, drops short/numeric tokens", () => {
|
|
30
|
+
const spec = `page.locator(".step-deduplication"); page.locator("#a"); page.getByText("42");`;
|
|
31
|
+
const t = extractSelectors(spec);
|
|
32
|
+
expect(t).toContain(".step-deduplication");
|
|
33
|
+
expect(t).not.toContain("#a");
|
|
34
|
+
expect(t).not.toContain("42");
|
|
35
|
+
});
|
|
36
|
+
it("returns [] for content with no selectors", () => {
|
|
37
|
+
expect(extractSelectors("const x = 1;")).toEqual([]);
|
|
38
|
+
});
|
|
39
|
+
it("matches the test-id attribute family (data-testid default, data-qa, data-cy)", () => {
|
|
40
|
+
const spec = `page.locator('[data-testid="submit-order"]'); page.locator('[data-qa="cart-total"]'); page.locator('[data-cy="nav-home"]');`;
|
|
41
|
+
const t = extractSelectors(spec);
|
|
42
|
+
expect(t).toEqual(expect.arrayContaining(["submit-order", "cart-total", "nav-home"]));
|
|
43
|
+
});
|
|
44
|
+
it("accepts extra custom attributes", () => {
|
|
45
|
+
const t = extractSelectors(`page.locator('[data-pw="widget-x"]');`, ["data-pw"]);
|
|
46
|
+
expect(t).toContain("widget-x");
|
|
47
|
+
});
|
|
48
|
+
it("keeps raw css locators that merely contain a family-attr substring", () => {
|
|
49
|
+
const t = extractSelectors(`page.locator(".data-testing-notes"); page.locator(".data-qatar-widget"); page.locator(".data-cycle-indicator");`);
|
|
50
|
+
expect(t).toEqual(expect.arrayContaining([".data-testing-notes", ".data-qatar-widget", ".data-cycle-indicator"]));
|
|
51
|
+
});
|
|
52
|
+
it("matches locator strings containing the other quote type (has-text idiom)", () => {
|
|
53
|
+
const spec = `page.locator('button:has-text("資産登録")'); page.locator("li:has-text('確認')");`;
|
|
54
|
+
const t = extractSelectors(spec);
|
|
55
|
+
expect(t).toEqual(expect.arrayContaining(['button:has-text("資産登録")', "li:has-text('確認')"]));
|
|
56
|
+
});
|
|
57
|
+
it("still dedups bracketed test-id raw locators regardless of quote nesting", () => {
|
|
58
|
+
const t = extractSelectors(`page.locator('[data-test="pay-now"]');`);
|
|
59
|
+
expect(t).toEqual(["pay-now"]); // attribute value only — raw form still skipped
|
|
60
|
+
});
|
|
61
|
+
it("extracts frameLocator selector args (iframe-scoped POM evidence)", () => {
|
|
62
|
+
const spec = `page.frameLocator('iframe#asset-list').locator('button:has-text("資産登録")').click();
|
|
63
|
+
page.frameLocator("#application-new-applied").getByRole("button", { name: "提出" });`;
|
|
64
|
+
const t = extractSelectors(spec);
|
|
65
|
+
expect(t).toEqual(expect.arrayContaining(["iframe#asset-list", "#application-new-applied", 'button:has-text("資産登録")']));
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type PomBinding = {
|
|
2
|
+
kind: "instance";
|
|
3
|
+
name: string;
|
|
4
|
+
classFile: string;
|
|
5
|
+
} | {
|
|
6
|
+
kind: "function";
|
|
7
|
+
name: string;
|
|
8
|
+
sourceFile: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function extractPomBindings(specContent: string, opts: {
|
|
11
|
+
specDir: string;
|
|
12
|
+
pomFiles: string[];
|
|
13
|
+
pageManagerFile?: string;
|
|
14
|
+
/** Prefix -> absolute dir, for resolving non-relative specifiers (tsconfig/jsconfig `paths`). */
|
|
15
|
+
aliasMap?: Record<string, string>;
|
|
16
|
+
}): Promise<{
|
|
17
|
+
bindings: PomBinding[];
|
|
18
|
+
unrecognized: string[];
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { stripComments, stripStrings } from "./strip.js";
|
|
4
|
+
/** Matches `import { A, B } from '<ref>'` and `const { A, B } = require('<ref>')`. */
|
|
5
|
+
const IMPORT_RE = /import\s*\{([^}]+)\}\s*from\s*["']([^"']+)["']|const\s*\{([^}]+)\}\s*=\s*require\(\s*["']([^"']+)["']\s*\)/g;
|
|
6
|
+
/** Matches `import X from '<ref>'` (default import) and `const X = require('<ref>')` (non-destructured). */
|
|
7
|
+
const DEFAULT_IMPORT_RE = /import\s+(\w+)\s*from\s*["']([^"']+)["']|const\s+(\w+)\s*=\s*require\(\s*["']([^"']+)["']\s*\)/g;
|
|
8
|
+
/** `const v = new ClassName(...)` / `let` / `var`. */
|
|
9
|
+
const NEW_RE = /\b(?:const|let|var)\s+(\w+)\s*=\s*new\s+(\w+)\s*\(/g;
|
|
10
|
+
/** Inside a page-manager file: `this.member = new ClassName(...)`. */
|
|
11
|
+
const PM_MEMBER_RE = /this\.(\w+)\s*=\s*new\s+(\w+)\s*\(/g;
|
|
12
|
+
function escapeRegExp(s) {
|
|
13
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
14
|
+
}
|
|
15
|
+
/** One-hop import resolution against the known POM set (no existence check beyond membership).
|
|
16
|
+
* Relative specifiers (`./`, `../`) resolve against `fromDir`; non-relative specifiers resolve
|
|
17
|
+
* through `aliasMap` (longest-prefix match, e.g. `@pageobjects/*` -> an absolute dir) when given. */
|
|
18
|
+
function resolveAgainstPomSet(fromDir, ref, pomSet, aliasMap) {
|
|
19
|
+
let base;
|
|
20
|
+
if (ref.startsWith(".")) {
|
|
21
|
+
base = path.resolve(fromDir, ref);
|
|
22
|
+
}
|
|
23
|
+
else if (aliasMap) {
|
|
24
|
+
const prefix = Object.keys(aliasMap)
|
|
25
|
+
.filter((p) => ref === p || ref.startsWith(`${p}/`))
|
|
26
|
+
.sort((a, b) => b.length - a.length)[0];
|
|
27
|
+
if (!prefix)
|
|
28
|
+
return undefined;
|
|
29
|
+
base = path.resolve(aliasMap[prefix], ref.slice(prefix.length).replace(/^\//, ""));
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
for (const cand of [base, `${base}.ts`, `${base}.js`, base.replace(/\.js$/, ".ts")]) {
|
|
35
|
+
if (pomSet.has(cand))
|
|
36
|
+
return cand;
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
/** Parses `import {...} from '...'` / `require(...)` (named/destructured) and `import X from '...'` /
|
|
41
|
+
* `const X = require('...')` (default/non-destructured) statements, mapping each local name to its
|
|
42
|
+
* resolved POM file (or `undefined` if imported but not a recognized POM file). */
|
|
43
|
+
function collectImportMap(content, fromDir, pomSet, aliasMap) {
|
|
44
|
+
const importMap = new Map();
|
|
45
|
+
for (const m of content.matchAll(IMPORT_RE)) {
|
|
46
|
+
const names = m[1] ?? m[3];
|
|
47
|
+
const ref = m[2] ?? m[4];
|
|
48
|
+
if (!names || !ref)
|
|
49
|
+
continue;
|
|
50
|
+
const resolved = resolveAgainstPomSet(fromDir, ref, pomSet, aliasMap);
|
|
51
|
+
for (const raw of names.split(",")) {
|
|
52
|
+
const name = raw.trim();
|
|
53
|
+
if (name)
|
|
54
|
+
importMap.set(name, resolved);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const m of content.matchAll(DEFAULT_IMPORT_RE)) {
|
|
58
|
+
const name = m[1] ?? m[3];
|
|
59
|
+
const ref = m[2] ?? m[4];
|
|
60
|
+
if (!name || !ref)
|
|
61
|
+
continue;
|
|
62
|
+
importMap.set(name, resolveAgainstPomSet(fromDir, ref, pomSet, aliasMap));
|
|
63
|
+
}
|
|
64
|
+
return importMap;
|
|
65
|
+
}
|
|
66
|
+
export async function extractPomBindings(specContent, opts) {
|
|
67
|
+
const pomSet = new Set(opts.pomFiles);
|
|
68
|
+
const bindings = [];
|
|
69
|
+
const seenBindings = new Set();
|
|
70
|
+
const unrecognized = [];
|
|
71
|
+
const seenUnrecognized = new Set();
|
|
72
|
+
const pushBinding = (b) => {
|
|
73
|
+
const key = `${b.kind}:${b.name}`;
|
|
74
|
+
if (seenBindings.has(key))
|
|
75
|
+
return;
|
|
76
|
+
seenBindings.add(key);
|
|
77
|
+
bindings.push(b);
|
|
78
|
+
};
|
|
79
|
+
const pushUnrecognized = (s) => {
|
|
80
|
+
if (seenUnrecognized.has(s))
|
|
81
|
+
return;
|
|
82
|
+
seenUnrecognized.add(s);
|
|
83
|
+
unrecognized.push(s);
|
|
84
|
+
};
|
|
85
|
+
// Strip comments first (string/template literals stay intact so a real import's quoted path
|
|
86
|
+
// is still readable), collect imports, then blank strings too before scanning for `new`/call/
|
|
87
|
+
// pm-var syntax -- so neither commented-out code nor code-shaped prose sitting inside a JS
|
|
88
|
+
// string can be mistaken for a real binding.
|
|
89
|
+
const specNoComments = stripComments(specContent);
|
|
90
|
+
const importMap = collectImportMap(specNoComments, opts.specDir, pomSet, opts.aliasMap);
|
|
91
|
+
const spec = stripStrings(specNoComments);
|
|
92
|
+
// `new ClassName(...)` assigned to a variable -> instance binding (or unrecognized if the
|
|
93
|
+
// imported class isn't a recognized POM file).
|
|
94
|
+
const usedWithNew = new Set();
|
|
95
|
+
for (const m of spec.matchAll(NEW_RE)) {
|
|
96
|
+
const [, varName, className] = m;
|
|
97
|
+
usedWithNew.add(className);
|
|
98
|
+
if (!importMap.has(className))
|
|
99
|
+
continue; // not an imported name at all -- not our concern
|
|
100
|
+
const resolved = importMap.get(className);
|
|
101
|
+
if (resolved) {
|
|
102
|
+
pushBinding({ kind: "instance", name: varName, classFile: resolved });
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
pushUnrecognized(varName);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Imported names resolving to a POM file, never used with `new`, called directly -> free function.
|
|
109
|
+
for (const [name, resolved] of importMap) {
|
|
110
|
+
if (!resolved || usedWithNew.has(name))
|
|
111
|
+
continue;
|
|
112
|
+
const directCallRe = new RegExp(`(?<!\\.)\\b${escapeRegExp(name)}\\s*\\(`);
|
|
113
|
+
if (directCallRe.test(spec)) {
|
|
114
|
+
pushBinding({ kind: "function", name, sourceFile: resolved });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Page-manager convention: `this.member = new Class(...)` inside pageManagerFile, exposed as
|
|
118
|
+
// `<pmVar>.<member>` bindings for every `const <pmVar> = new PageManager(...)` in the spec.
|
|
119
|
+
if (opts.pageManagerFile) {
|
|
120
|
+
let pmContentRaw;
|
|
121
|
+
try {
|
|
122
|
+
pmContentRaw = await readFile(opts.pageManagerFile, "utf8");
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
pmContentRaw = undefined;
|
|
126
|
+
}
|
|
127
|
+
if (pmContentRaw) {
|
|
128
|
+
const pmNoComments = stripComments(pmContentRaw);
|
|
129
|
+
const pmImportMap = collectImportMap(pmNoComments, path.dirname(opts.pageManagerFile), pomSet, opts.aliasMap);
|
|
130
|
+
const pmContent = stripStrings(pmNoComments);
|
|
131
|
+
// `export class X` (ESM) or plain `class X { ... } ... module.exports = X` (CJS, the real
|
|
132
|
+
// oo-testbed convention) -- the `export` keyword is optional.
|
|
133
|
+
const pmClassName = pmContent.match(/(?:export\s+)?class\s+(\w+)/)?.[1];
|
|
134
|
+
if (pmClassName) {
|
|
135
|
+
const pmVarRe = new RegExp(`\\b(?:const|let|var)\\s+(\\w+)\\s*=\\s*new\\s+${escapeRegExp(pmClassName)}\\s*\\(`, "g");
|
|
136
|
+
const pmVars = [...spec.matchAll(pmVarRe)].map((m) => m[1]);
|
|
137
|
+
if (pmVars.length > 0) {
|
|
138
|
+
const memberMap = new Map();
|
|
139
|
+
for (const m of pmContent.matchAll(PM_MEMBER_RE)) {
|
|
140
|
+
const [, member, className] = m;
|
|
141
|
+
const resolved = pmImportMap.get(className);
|
|
142
|
+
if (resolved)
|
|
143
|
+
memberMap.set(member, resolved);
|
|
144
|
+
}
|
|
145
|
+
for (const pmVar of pmVars) {
|
|
146
|
+
for (const [member, classFile] of memberMap) {
|
|
147
|
+
pushBinding({ kind: "instance", name: `${pmVar}.${member}`, classFile });
|
|
148
|
+
}
|
|
149
|
+
const accessRe = new RegExp(`\\b${escapeRegExp(pmVar)}\\.(\\w+)`, "g");
|
|
150
|
+
for (const am of spec.matchAll(accessRe)) {
|
|
151
|
+
const member = am[1];
|
|
152
|
+
if (!memberMap.has(member))
|
|
153
|
+
pushUnrecognized(`${pmVar}.${member}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { bindings, unrecognized };
|
|
161
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
3
|
+
import { extractPomBindings } from "./bindings.js";
|
|
4
|
+
const OO = path.join(path.dirname(fileURLToPath(import.meta.url)), "__fixtures__", "oo-style");
|
|
5
|
+
const AF = path.join(path.dirname(fileURLToPath(import.meta.url)), "__fixtures__", "af-style");
|
|
6
|
+
const OO_POM_FILES = [
|
|
7
|
+
path.join(OO, "page-manager.js"),
|
|
8
|
+
path.join(OO, "loginPage.js"),
|
|
9
|
+
path.join(OO, "widgetsPage.js"),
|
|
10
|
+
path.join(OO, "widgetHelpers.js"),
|
|
11
|
+
path.join(OO, "commonActions.js"),
|
|
12
|
+
];
|
|
13
|
+
const AF_POM_FILES = [
|
|
14
|
+
path.join(AF, "asset-list.page.ts"),
|
|
15
|
+
path.join(AF, "workflow-footer.page.ts"),
|
|
16
|
+
path.join(AF, "report.iframe.page.ts"),
|
|
17
|
+
];
|
|
18
|
+
describe("extractPomBindings -- oo-style (page-manager convention)", () => {
|
|
19
|
+
it("binds every <pm>.<member> when the spec instantiates the page manager", async () => {
|
|
20
|
+
const spec = `
|
|
21
|
+
import { PageManager } from './page-manager.js';
|
|
22
|
+
const pm = new PageManager(page);
|
|
23
|
+
await pm.loginPage.signIn.click();
|
|
24
|
+
`;
|
|
25
|
+
const { bindings, unrecognized } = await extractPomBindings(spec, {
|
|
26
|
+
specDir: OO,
|
|
27
|
+
pomFiles: OO_POM_FILES,
|
|
28
|
+
pageManagerFile: path.join(OO, "page-manager.js"),
|
|
29
|
+
});
|
|
30
|
+
expect(bindings).toEqual(expect.arrayContaining([
|
|
31
|
+
{ kind: "instance", name: "pm.loginPage", classFile: path.join(OO, "loginPage.js") },
|
|
32
|
+
{ kind: "instance", name: "pm.widgetsPage", classFile: path.join(OO, "widgetsPage.js") },
|
|
33
|
+
]));
|
|
34
|
+
expect(unrecognized).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
it("flags an unknown page-manager member as unrecognized instead of fabricating a binding", async () => {
|
|
37
|
+
const spec = `
|
|
38
|
+
import { PageManager } from './page-manager.js';
|
|
39
|
+
const pm = new PageManager(page);
|
|
40
|
+
await pm.reportsPage.export();
|
|
41
|
+
`;
|
|
42
|
+
const { bindings, unrecognized } = await extractPomBindings(spec, {
|
|
43
|
+
specDir: OO,
|
|
44
|
+
pomFiles: OO_POM_FILES,
|
|
45
|
+
pageManagerFile: path.join(OO, "page-manager.js"),
|
|
46
|
+
});
|
|
47
|
+
expect(bindings.some((b) => b.name.includes("reportsPage"))).toBe(false);
|
|
48
|
+
expect(unrecognized).toContain("pm.reportsPage");
|
|
49
|
+
});
|
|
50
|
+
it("binds a direct `new` instantiation of an imported POM class (no page manager)", async () => {
|
|
51
|
+
const spec = `
|
|
52
|
+
import { LoginPage } from './loginPage.js';
|
|
53
|
+
const login = new LoginPage(page);
|
|
54
|
+
await login.signIn.click();
|
|
55
|
+
`;
|
|
56
|
+
const { bindings, unrecognized } = await extractPomBindings(spec, { specDir: OO, pomFiles: OO_POM_FILES });
|
|
57
|
+
expect(bindings).toEqual([{ kind: "instance", name: "login", classFile: path.join(OO, "loginPage.js") }]);
|
|
58
|
+
expect(unrecognized).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
it("binds an imported free function used as a direct call", async () => {
|
|
61
|
+
const spec = `
|
|
62
|
+
import { openNavFlyout } from './commonActions.js';
|
|
63
|
+
await openNavFlyout(page);
|
|
64
|
+
`;
|
|
65
|
+
const { bindings, unrecognized } = await extractPomBindings(spec, { specDir: OO, pomFiles: OO_POM_FILES });
|
|
66
|
+
expect(bindings).toEqual([{ kind: "function", name: "openNavFlyout", sourceFile: path.join(OO, "commonActions.js") }]);
|
|
67
|
+
expect(unrecognized).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
it("never fabricates a binding for `new` on an import that isn't a recognized POM file", async () => {
|
|
70
|
+
const spec = `
|
|
71
|
+
import { HelperNonPom } from './helperNonPom.js';
|
|
72
|
+
const helper = new HelperNonPom();
|
|
73
|
+
`;
|
|
74
|
+
// helperNonPom.js intentionally excluded from pomFiles -- it's a non-POM utility.
|
|
75
|
+
const { bindings, unrecognized } = await extractPomBindings(spec, { specDir: OO, pomFiles: OO_POM_FILES });
|
|
76
|
+
expect(bindings).toEqual([]);
|
|
77
|
+
expect(unrecognized).toContain("helper");
|
|
78
|
+
});
|
|
79
|
+
it("ignores commented-out import/new/call syntax instead of fabricating a binding from it", async () => {
|
|
80
|
+
const spec = `
|
|
81
|
+
// import { LoginPage } from './loginPage.js'; const login = new LoginPage(page);
|
|
82
|
+
/* const helper2 = new LoginPage(page); */
|
|
83
|
+
const note = "import { LoginPage } from './loginPage.js'; const login2 = new LoginPage(page);";
|
|
84
|
+
import { openNavFlyout } from './commonActions.js';
|
|
85
|
+
// await openNavFlyout(page);
|
|
86
|
+
`;
|
|
87
|
+
const { bindings, unrecognized } = await extractPomBindings(spec, { specDir: OO, pomFiles: OO_POM_FILES });
|
|
88
|
+
expect(bindings).toEqual([]);
|
|
89
|
+
expect(unrecognized).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe("extractPomBindings -- oo-style (CJS page-manager convention)", () => {
|
|
93
|
+
it("binds pm.<member> from a page-manager file with no `export` on its class, a default import, and an extensionless require -- the real oo-testbed shape", async () => {
|
|
94
|
+
const spec = `
|
|
95
|
+
const PageManager = require('./page-manager-cjs.js');
|
|
96
|
+
const pm = new PageManager(page);
|
|
97
|
+
await pm.loginPage.signInAs(user);
|
|
98
|
+
await pm.widgetsPage.toggle('foo');
|
|
99
|
+
`;
|
|
100
|
+
const { bindings, unrecognized } = await extractPomBindings(spec, {
|
|
101
|
+
specDir: OO,
|
|
102
|
+
pomFiles: [...OO_POM_FILES, path.join(OO, "page-manager-cjs.js")],
|
|
103
|
+
pageManagerFile: path.join(OO, "page-manager-cjs.js"),
|
|
104
|
+
});
|
|
105
|
+
expect(bindings).toEqual(expect.arrayContaining([
|
|
106
|
+
{ kind: "instance", name: "pm.loginPage", classFile: path.join(OO, "loginPage.js") },
|
|
107
|
+
{ kind: "instance", name: "pm.widgetsPage", classFile: path.join(OO, "widgetsPage.js") },
|
|
108
|
+
]));
|
|
109
|
+
expect(unrecognized).toEqual([]);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe("extractPomBindings -- alias-import resolution", () => {
|
|
113
|
+
const AF_PAGEOBJECTS = path.join(AF, "pageobjects");
|
|
114
|
+
const ASSET_LIST_ALIAS_FILE = path.join(AF_PAGEOBJECTS, "asset-list-page.ts");
|
|
115
|
+
it("resolves a non-relative specifier through aliasMap when the target is a recognized POM file", async () => {
|
|
116
|
+
const spec = `
|
|
117
|
+
import { AssetListPage } from '@pageobjects/asset-list-page';
|
|
118
|
+
const list = new AssetListPage(page);
|
|
119
|
+
await list.clickSearch();
|
|
120
|
+
`;
|
|
121
|
+
const { bindings, unrecognized } = await extractPomBindings(spec, {
|
|
122
|
+
specDir: AF,
|
|
123
|
+
pomFiles: [ASSET_LIST_ALIAS_FILE],
|
|
124
|
+
aliasMap: { "@pageobjects": AF_PAGEOBJECTS },
|
|
125
|
+
});
|
|
126
|
+
expect(bindings).toEqual([{ kind: "instance", name: "list", classFile: ASSET_LIST_ALIAS_FILE }]);
|
|
127
|
+
expect(unrecognized).toEqual([]);
|
|
128
|
+
});
|
|
129
|
+
it("never fabricates a binding through an alias when the resolved file isn't in pomFiles (safety contract)", async () => {
|
|
130
|
+
const spec = `
|
|
131
|
+
import { AssetListPage } from '@pageobjects/asset-list-page';
|
|
132
|
+
const list = new AssetListPage(page);
|
|
133
|
+
`;
|
|
134
|
+
const { bindings, unrecognized } = await extractPomBindings(spec, {
|
|
135
|
+
specDir: AF,
|
|
136
|
+
pomFiles: [], // asset-list-page.ts intentionally excluded
|
|
137
|
+
aliasMap: { "@pageobjects": AF_PAGEOBJECTS },
|
|
138
|
+
});
|
|
139
|
+
expect(bindings).toEqual([]);
|
|
140
|
+
expect(unrecognized).toContain("list");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe("extractPomBindings -- af-style (direct instantiation)", () => {
|
|
144
|
+
it("binds a direct-instantiation class with a component property chain", async () => {
|
|
145
|
+
const spec = `
|
|
146
|
+
import { AssetListPage } from './asset-list.page.js';
|
|
147
|
+
const list = new AssetListPage(page);
|
|
148
|
+
await list.footer.okBtn.click();
|
|
149
|
+
`;
|
|
150
|
+
const { bindings, unrecognized } = await extractPomBindings(spec, { specDir: AF, pomFiles: AF_POM_FILES });
|
|
151
|
+
expect(bindings).toEqual([{ kind: "instance", name: "list", classFile: path.join(AF, "asset-list.page.ts") }]);
|
|
152
|
+
expect(unrecognized).toEqual([]);
|
|
153
|
+
});
|
|
154
|
+
it("binds a direct-instantiation iframe-scoped class the same way", async () => {
|
|
155
|
+
const spec = `
|
|
156
|
+
import { ReportIframePage } from './report.iframe.page.js';
|
|
157
|
+
const report = new ReportIframePage(frame);
|
|
158
|
+
await report.exportBtn.click();
|
|
159
|
+
`;
|
|
160
|
+
const { bindings, unrecognized } = await extractPomBindings(spec, { specDir: AF, pomFiles: AF_POM_FILES });
|
|
161
|
+
expect(bindings).toEqual([{ kind: "instance", name: "report", classFile: path.join(AF, "report.iframe.page.ts") }]);
|
|
162
|
+
expect(unrecognized).toEqual([]);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PomBinding } from "./bindings.js";
|
|
2
|
+
export type PomCall = {
|
|
3
|
+
binding: string;
|
|
4
|
+
chain: string[];
|
|
5
|
+
strict: boolean;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* For each binding, finds member-access chains off its name in the spec: `name.method(...)`,
|
|
9
|
+
* `name.a.b(...)`, and bare property reads like `expect(name.toast)` (property-only uses count
|
|
10
|
+
* as calls too). Chains are matched greedily (longest run of `.identifier` segments per
|
|
11
|
+
* occurrence), and bindings are matched longest-name-first with the matched span "consumed" so a
|
|
12
|
+
* shorter binding name that happens to be a suffix of a longer one (e.g. `loginPage` vs.
|
|
13
|
+
* `pm.loginPage`) can't double-count the same occurrence. Comments and string/template literals
|
|
14
|
+
* are blanked out first so prose mentioning a binding's name never fabricates a call.
|
|
15
|
+
*/
|
|
16
|
+
export declare function extractPomCalls(specContent: string, bindings: PomBinding[]): PomCall[];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { stripCommentsAndStrings } from "./strip.js";
|
|
2
|
+
function escapeRegExp(s) {
|
|
3
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* For each binding, finds member-access chains off its name in the spec: `name.method(...)`,
|
|
7
|
+
* `name.a.b(...)`, and bare property reads like `expect(name.toast)` (property-only uses count
|
|
8
|
+
* as calls too). Chains are matched greedily (longest run of `.identifier` segments per
|
|
9
|
+
* occurrence), and bindings are matched longest-name-first with the matched span "consumed" so a
|
|
10
|
+
* shorter binding name that happens to be a suffix of a longer one (e.g. `loginPage` vs.
|
|
11
|
+
* `pm.loginPage`) can't double-count the same occurrence. Comments and string/template literals
|
|
12
|
+
* are blanked out first so prose mentioning a binding's name never fabricates a call.
|
|
13
|
+
*/
|
|
14
|
+
export function extractPomCalls(specContent, bindings) {
|
|
15
|
+
const content = stripCommentsAndStrings(specContent);
|
|
16
|
+
const sorted = [...bindings].sort((a, b) => b.name.length - a.name.length);
|
|
17
|
+
const consumed = [];
|
|
18
|
+
const isConsumed = (start) => consumed.some(([s, e]) => start >= s && start < e);
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
const found = [];
|
|
21
|
+
for (const binding of sorted) {
|
|
22
|
+
const escaped = escapeRegExp(binding.name);
|
|
23
|
+
const pattern = binding.kind === "function"
|
|
24
|
+
? new RegExp(`(?<!\\.)\\b${escaped}\\s*\\(`, "g")
|
|
25
|
+
: new RegExp(`\\b${escaped}((?:\\.[A-Za-z_$][\\w$]*)+)`, "g");
|
|
26
|
+
let m;
|
|
27
|
+
while ((m = pattern.exec(content))) {
|
|
28
|
+
const start = m.index;
|
|
29
|
+
const end = start + m[0].length;
|
|
30
|
+
if (isConsumed(start))
|
|
31
|
+
continue;
|
|
32
|
+
consumed.push([start, end]);
|
|
33
|
+
const chain = binding.kind === "function" ? [] : m[1].split(".").filter(Boolean);
|
|
34
|
+
const key = `${binding.name}|${chain.join(".")}`;
|
|
35
|
+
if (seen.has(key))
|
|
36
|
+
continue;
|
|
37
|
+
seen.add(key);
|
|
38
|
+
found.push({ binding: binding.name, chain, strict: chain.length <= 1, pos: start });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return found.sort((a, b) => a.pos - b.pos).map(({ binding, chain, strict }) => ({ binding, chain, strict }));
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|