@beignet/cli 0.0.49 → 0.0.51
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/CHANGELOG.md +32 -0
- package/README.md +54 -5
- package/dist/analysis/workspace.d.ts +1 -0
- package/dist/analysis/workspace.d.ts.map +1 -1
- package/dist/analysis/workspace.js +20 -10
- package/dist/analysis/workspace.js.map +1 -1
- package/dist/app-map-changes.d.ts +103 -0
- package/dist/app-map-changes.d.ts.map +1 -0
- package/dist/app-map-changes.js +949 -0
- package/dist/app-map-changes.js.map +1 -0
- package/dist/git-changes.d.ts +30 -0
- package/dist/git-changes.d.ts.map +1 -0
- package/dist/git-changes.js +367 -0
- package/dist/git-changes.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -4
- package/dist/index.js.map +1 -1
- package/dist/inspect.js +413 -43
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +3 -0
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -0
- package/dist/lib.js.map +1 -1
- package/dist/make/shared.d.ts.map +1 -1
- package/dist/make/shared.js +31 -27
- package/dist/make/shared.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +0 -2
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +46 -6
- package/dist/mcp.js.map +1 -1
- package/dist/operational-process.d.ts +1 -0
- package/dist/operational-process.d.ts.map +1 -1
- package/dist/operational-process.js.map +1 -1
- package/dist/operational-runner.js +1 -0
- package/dist/operational-runner.js.map +1 -1
- package/dist/outbox.d.ts +1 -0
- package/dist/outbox.d.ts.map +1 -1
- package/dist/outbox.js +58 -12
- package/dist/outbox.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +28 -3
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +4 -1
- package/dist/templates/base.js.map +1 -1
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +30 -5
- package/src/analysis/workspace.ts +25 -9
- package/src/app-map-changes.ts +1462 -0
- package/src/git-changes.ts +511 -0
- package/src/index.ts +84 -4
- package/src/inspect.ts +586 -51
- package/src/lib.ts +21 -0
- package/src/make/shared.ts +31 -27
- package/src/make.ts +0 -2
- package/src/mcp.ts +65 -12
- package/src/operational-process.ts +1 -0
- package/src/operational-runner.ts +1 -0
- package/src/outbox.ts +63 -12
- package/src/templates/agents.ts +28 -3
- package/src/templates/base.ts +4 -1
package/src/inspect.ts
CHANGED
|
@@ -1576,36 +1576,57 @@ function parseRouteExports(
|
|
|
1576
1576
|
config: ResolvedBeignetConfig,
|
|
1577
1577
|
): RouteExport[] {
|
|
1578
1578
|
const exports: RouteExport[] = [];
|
|
1579
|
-
const
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1579
|
+
const sourceFile = ts.createSourceFile(
|
|
1580
|
+
handlerFile,
|
|
1581
|
+
source,
|
|
1582
|
+
ts.ScriptTarget.Latest,
|
|
1583
|
+
true,
|
|
1584
|
+
ts.ScriptKind.TS,
|
|
1585
|
+
);
|
|
1586
|
+
const imports = parseRouteNamedImports(sourceFile, config, handlerFile);
|
|
1584
1587
|
|
|
1585
|
-
for (const
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1588
|
+
for (const statement of sourceFile.statements) {
|
|
1589
|
+
if (
|
|
1590
|
+
!ts.isVariableStatement(statement) ||
|
|
1591
|
+
!statement.modifiers?.some(
|
|
1592
|
+
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword,
|
|
1593
|
+
)
|
|
1594
|
+
) {
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1590
1597
|
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1599
|
+
if (
|
|
1600
|
+
!declaration.initializer ||
|
|
1601
|
+
!ts.isObjectBindingPattern(declaration.name) ||
|
|
1602
|
+
!isNamedRouteFactoryCall(declaration.initializer, "createApiRoute")
|
|
1603
|
+
) {
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
for (const element of declaration.name.elements) {
|
|
1608
|
+
if (!ts.isIdentifier(element.name)) continue;
|
|
1609
|
+
const method = element.name.text;
|
|
1610
|
+
if (!isHttpMethod(method)) continue;
|
|
1611
|
+
exports.push({
|
|
1612
|
+
method,
|
|
1613
|
+
handlerFile,
|
|
1614
|
+
contractRef: routePath,
|
|
1615
|
+
catchAllPrefix: catchAllRoutePrefix(routePath),
|
|
1616
|
+
source: "next-route",
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1598
1619
|
}
|
|
1599
1620
|
}
|
|
1600
1621
|
|
|
1601
|
-
for (const
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1622
|
+
for (const { exportName, declaration } of exportedVariableDeclarations(
|
|
1623
|
+
sourceFile,
|
|
1624
|
+
)) {
|
|
1625
|
+
if (!isHttpMethod(exportName) || !declaration.initializer) continue;
|
|
1626
|
+
const method = exportName;
|
|
1627
|
+
const localName = routeLocalContractIdentifier(declaration.initializer);
|
|
1606
1628
|
|
|
1607
|
-
if (
|
|
1608
|
-
const localName = contractMatch[1];
|
|
1629
|
+
if (localName) {
|
|
1609
1630
|
const imported = imports.get(localName);
|
|
1610
1631
|
exports.push({
|
|
1611
1632
|
method,
|
|
@@ -1617,7 +1638,7 @@ function parseRouteExports(
|
|
|
1617
1638
|
continue;
|
|
1618
1639
|
}
|
|
1619
1640
|
|
|
1620
|
-
if (
|
|
1641
|
+
if (isServerApiExpression(declaration.initializer)) {
|
|
1621
1642
|
exports.push({
|
|
1622
1643
|
method,
|
|
1623
1644
|
handlerFile,
|
|
@@ -1631,6 +1652,98 @@ function parseRouteExports(
|
|
|
1631
1652
|
return exports;
|
|
1632
1653
|
}
|
|
1633
1654
|
|
|
1655
|
+
function parseRouteNamedImports(
|
|
1656
|
+
sourceFile: ts.SourceFile,
|
|
1657
|
+
config: ResolvedBeignetConfig,
|
|
1658
|
+
handlerFile: string,
|
|
1659
|
+
): Map<string, { importedName: string; contractFile?: string }> {
|
|
1660
|
+
const imports = new Map<
|
|
1661
|
+
string,
|
|
1662
|
+
{ importedName: string; contractFile?: string }
|
|
1663
|
+
>();
|
|
1664
|
+
|
|
1665
|
+
for (const statement of sourceFile.statements) {
|
|
1666
|
+
if (
|
|
1667
|
+
!ts.isImportDeclaration(statement) ||
|
|
1668
|
+
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
1669
|
+
statement.importClause?.isTypeOnly ||
|
|
1670
|
+
!statement.importClause?.namedBindings ||
|
|
1671
|
+
!ts.isNamedImports(statement.importClause.namedBindings)
|
|
1672
|
+
) {
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
const contractFile = contractFileFromImport(
|
|
1677
|
+
statement.moduleSpecifier.text,
|
|
1678
|
+
config,
|
|
1679
|
+
handlerFile,
|
|
1680
|
+
);
|
|
1681
|
+
for (const element of statement.importClause.namedBindings.elements) {
|
|
1682
|
+
if (element.isTypeOnly) continue;
|
|
1683
|
+
imports.set(element.name.text, {
|
|
1684
|
+
importedName: element.propertyName?.text ?? element.name.text,
|
|
1685
|
+
contractFile,
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
return imports;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
function isNamedRouteFactoryCall(
|
|
1694
|
+
expression: ts.Expression,
|
|
1695
|
+
name: string,
|
|
1696
|
+
): boolean {
|
|
1697
|
+
const unwrapped = unwrapContractExpression(expression);
|
|
1698
|
+
if (!ts.isCallExpression(unwrapped)) return false;
|
|
1699
|
+
const callee = unwrapContractExpression(unwrapped.expression);
|
|
1700
|
+
return ts.isIdentifier(callee) && callee.text === name;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
function routeLocalContractIdentifier(
|
|
1704
|
+
expression: ts.Expression,
|
|
1705
|
+
): string | undefined {
|
|
1706
|
+
const handleCall = unwrapContractExpression(expression);
|
|
1707
|
+
if (!ts.isCallExpression(handleCall)) return undefined;
|
|
1708
|
+
const handleAccess = unwrapContractExpression(handleCall.expression);
|
|
1709
|
+
if (
|
|
1710
|
+
!ts.isPropertyAccessExpression(handleAccess) ||
|
|
1711
|
+
handleAccess.name.text !== "handle"
|
|
1712
|
+
) {
|
|
1713
|
+
return undefined;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
const routeCall = unwrapContractExpression(handleAccess.expression);
|
|
1717
|
+
if (!ts.isCallExpression(routeCall)) return undefined;
|
|
1718
|
+
const routeAccess = unwrapContractExpression(routeCall.expression);
|
|
1719
|
+
if (
|
|
1720
|
+
!ts.isPropertyAccessExpression(routeAccess) ||
|
|
1721
|
+
routeAccess.name.text !== "route" ||
|
|
1722
|
+
!ts.isIdentifier(routeAccess.expression) ||
|
|
1723
|
+
routeAccess.expression.text !== "server"
|
|
1724
|
+
) {
|
|
1725
|
+
return undefined;
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
const contract = routeCall.arguments[0];
|
|
1729
|
+
const unwrappedContract = contract
|
|
1730
|
+
? unwrapContractExpression(contract)
|
|
1731
|
+
: undefined;
|
|
1732
|
+
return unwrappedContract && ts.isIdentifier(unwrappedContract)
|
|
1733
|
+
? unwrappedContract.text
|
|
1734
|
+
: undefined;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function isServerApiExpression(expression: ts.Expression): boolean {
|
|
1738
|
+
const unwrapped = unwrapContractExpression(expression);
|
|
1739
|
+
return (
|
|
1740
|
+
ts.isPropertyAccessExpression(unwrapped) &&
|
|
1741
|
+
ts.isIdentifier(unwrapped.expression) &&
|
|
1742
|
+
unwrapped.expression.text === "server" &&
|
|
1743
|
+
unwrapped.name.text === "api"
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1634
1747
|
function catchAllRoutePrefix(routePath: string): string | undefined {
|
|
1635
1748
|
const segments = routePath.split("/").filter(Boolean);
|
|
1636
1749
|
const catchAllIndex = segments.findIndex((segment) => segment.endsWith("*"));
|
|
@@ -1762,6 +1875,29 @@ function contractFileFromImport(
|
|
|
1762
1875
|
config: ResolvedBeignetConfig,
|
|
1763
1876
|
importerFile?: string,
|
|
1764
1877
|
): string | undefined {
|
|
1878
|
+
const resolveCandidate = (candidate: string) => {
|
|
1879
|
+
const extension = path.extname(candidate).toLowerCase();
|
|
1880
|
+
if (!extension) return `${candidate}.ts`;
|
|
1881
|
+
|
|
1882
|
+
let sourceExtension: string | undefined;
|
|
1883
|
+
switch (extension) {
|
|
1884
|
+
case ".js":
|
|
1885
|
+
sourceExtension = ".ts";
|
|
1886
|
+
break;
|
|
1887
|
+
case ".jsx":
|
|
1888
|
+
sourceExtension = ".tsx";
|
|
1889
|
+
break;
|
|
1890
|
+
case ".mjs":
|
|
1891
|
+
sourceExtension = ".mts";
|
|
1892
|
+
break;
|
|
1893
|
+
case ".cjs":
|
|
1894
|
+
sourceExtension = ".cts";
|
|
1895
|
+
break;
|
|
1896
|
+
}
|
|
1897
|
+
return sourceExtension
|
|
1898
|
+
? `${candidate.slice(0, -extension.length)}${sourceExtension}`
|
|
1899
|
+
: candidate;
|
|
1900
|
+
};
|
|
1765
1901
|
const contractsPath = directoryPath(config.paths.contracts);
|
|
1766
1902
|
const aliasPrefix = `@/${contractsPath}/`;
|
|
1767
1903
|
const aliasExact = `@/${contractsPath}`;
|
|
@@ -1772,18 +1908,18 @@ function contractFileFromImport(
|
|
|
1772
1908
|
return `${contractsPath}/index.ts`;
|
|
1773
1909
|
}
|
|
1774
1910
|
if (sourcePath.startsWith(aliasPrefix)) {
|
|
1775
|
-
return
|
|
1911
|
+
return resolveCandidate(sourcePath.slice("@/".length));
|
|
1776
1912
|
}
|
|
1777
1913
|
|
|
1778
1914
|
if (sourcePath.startsWith(relativePrefix)) {
|
|
1779
|
-
return
|
|
1915
|
+
return resolveCandidate(sourcePath);
|
|
1780
1916
|
}
|
|
1781
1917
|
|
|
1782
1918
|
if (importerFile && sourcePath.startsWith(".")) {
|
|
1783
1919
|
const resolved = normalizePath(
|
|
1784
1920
|
path.join(path.dirname(importerFile), sourcePath),
|
|
1785
1921
|
);
|
|
1786
|
-
return
|
|
1922
|
+
return resolveCandidate(resolved);
|
|
1787
1923
|
}
|
|
1788
1924
|
|
|
1789
1925
|
return undefined;
|
|
@@ -4404,6 +4540,7 @@ type WorkflowRegistrationDrift = {
|
|
|
4404
4540
|
unregistered: UnregisteredWorkflowRegistry[];
|
|
4405
4541
|
wiringFile?: string;
|
|
4406
4542
|
eventBusFile?: string;
|
|
4543
|
+
unsafeLifecycleFiles: string[];
|
|
4407
4544
|
};
|
|
4408
4545
|
};
|
|
4409
4546
|
|
|
@@ -4482,6 +4619,15 @@ async function inspectWorkflowRegistrationDrift(
|
|
|
4482
4619
|
}
|
|
4483
4620
|
|
|
4484
4621
|
const infraDir = directoryPath(path.dirname(config.paths.portWiring));
|
|
4622
|
+
for (const file of drift.listeners.unsafeLifecycleFiles) {
|
|
4623
|
+
diagnostics.push({
|
|
4624
|
+
severity: "warning",
|
|
4625
|
+
code: "BEIGNET_LISTENER_LIFECYCLE_UNSAFE",
|
|
4626
|
+
file,
|
|
4627
|
+
message: `${file} calls registerListeners(...) without the complete provider lifecycle, so server startup can resolve before listeners are ready or shutdown can leak subscriptions. Register listeners in start(), return or await registration.ready, and return or await registration.unsubscribe() in stop().`,
|
|
4628
|
+
});
|
|
4629
|
+
}
|
|
4630
|
+
|
|
4485
4631
|
const listenerTarget = drift.listeners.wiringFile
|
|
4486
4632
|
? `${drift.listeners.wiringFile}, which already calls registerListeners(...)`
|
|
4487
4633
|
: drift.listeners.eventBusFile
|
|
@@ -4652,7 +4798,7 @@ async function workflowRegistrationDrift(
|
|
|
4652
4798
|
events: [],
|
|
4653
4799
|
jobs: [],
|
|
4654
4800
|
},
|
|
4655
|
-
listeners: { unregistered: [] },
|
|
4801
|
+
listeners: { unregistered: [], unsafeLifecycleFiles: [] },
|
|
4656
4802
|
};
|
|
4657
4803
|
if (registries.length === 0) return drift;
|
|
4658
4804
|
|
|
@@ -4714,7 +4860,12 @@ async function workflowRegistrationDrift(
|
|
|
4714
4860
|
|
|
4715
4861
|
const listenerRegistries = byKind("listeners");
|
|
4716
4862
|
if (listenerRegistries.length > 0) {
|
|
4717
|
-
const wiring = await listenerWiringReferences(
|
|
4863
|
+
const wiring = await listenerWiringReferences(
|
|
4864
|
+
targetDir,
|
|
4865
|
+
files,
|
|
4866
|
+
config,
|
|
4867
|
+
listenerRegistries,
|
|
4868
|
+
);
|
|
4718
4869
|
drift.listeners = {
|
|
4719
4870
|
unregistered: unregisteredWorkflowRegistries(
|
|
4720
4871
|
listenerRegistries,
|
|
@@ -4722,6 +4873,7 @@ async function workflowRegistrationDrift(
|
|
|
4722
4873
|
),
|
|
4723
4874
|
wiringFile: wiring.wiringFile,
|
|
4724
4875
|
eventBusFile: wiring.eventBusFile,
|
|
4876
|
+
unsafeLifecycleFiles: wiring.unsafeLifecycleFiles,
|
|
4725
4877
|
};
|
|
4726
4878
|
}
|
|
4727
4879
|
|
|
@@ -5056,12 +5208,15 @@ async function listenerWiringReferences(
|
|
|
5056
5208
|
targetDir: string,
|
|
5057
5209
|
files: string[],
|
|
5058
5210
|
config: ResolvedBeignetConfig,
|
|
5211
|
+
listenerRegistries: FeatureWorkflowRegistry[],
|
|
5059
5212
|
): Promise<{
|
|
5060
5213
|
identifiers: Set<string>;
|
|
5061
5214
|
wiringFile?: string;
|
|
5062
5215
|
eventBusFile?: string;
|
|
5216
|
+
unsafeLifecycleFiles: string[];
|
|
5063
5217
|
}> {
|
|
5064
5218
|
const identifiers = new Set<string>();
|
|
5219
|
+
const unsafeLifecycleFiles = new Set<string>();
|
|
5065
5220
|
let wiringFile: string | undefined;
|
|
5066
5221
|
let eventBusFile: string | undefined;
|
|
5067
5222
|
let centralListenerRegistryReferenced = false;
|
|
@@ -5073,35 +5228,54 @@ async function listenerWiringReferences(
|
|
|
5073
5228
|
|
|
5074
5229
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
5075
5230
|
const namedImports = parseNamedImportSources(source);
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5231
|
+
const sourceFile = ts.createSourceFile(
|
|
5232
|
+
file,
|
|
5233
|
+
source,
|
|
5234
|
+
ts.ScriptTarget.Latest,
|
|
5235
|
+
true,
|
|
5236
|
+
file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
5237
|
+
);
|
|
5238
|
+
const registerListenersBindings = importedRegisterListenersBindings(
|
|
5239
|
+
source,
|
|
5240
|
+
namedImports,
|
|
5241
|
+
);
|
|
5242
|
+
const calls = registerListenersCalls(sourceFile, registerListenersBindings);
|
|
5243
|
+
let foundRelevantCall = false;
|
|
5085
5244
|
|
|
5086
|
-
|
|
5245
|
+
for (const call of calls) {
|
|
5246
|
+
const argsText = call.arguments
|
|
5247
|
+
.map((argument) => argument.getText(sourceFile))
|
|
5248
|
+
.join(",");
|
|
5087
5249
|
const callIdentifiers = identifiersFromArrayExpression(argsText);
|
|
5250
|
+
const referencesCentral = callReferencesCentralListenerRegistry({
|
|
5251
|
+
callIdentifiers,
|
|
5252
|
+
namedImports,
|
|
5253
|
+
importerFile: file,
|
|
5254
|
+
listenerRegistryFile,
|
|
5255
|
+
files,
|
|
5256
|
+
});
|
|
5257
|
+
const referencesFeature = callReferencesFeatureListenerRegistry({
|
|
5258
|
+
callIdentifiers,
|
|
5259
|
+
namedImports,
|
|
5260
|
+
importerFile: file,
|
|
5261
|
+
files,
|
|
5262
|
+
listenerRegistries,
|
|
5263
|
+
});
|
|
5264
|
+
if (!referencesCentral && !referencesFeature) continue;
|
|
5265
|
+
|
|
5266
|
+
foundRelevantCall = true;
|
|
5088
5267
|
for (const identifier of callIdentifiers) {
|
|
5089
5268
|
identifiers.add(identifier);
|
|
5090
5269
|
}
|
|
5091
|
-
if (
|
|
5092
|
-
callReferencesCentralListenerRegistry({
|
|
5093
|
-
callIdentifiers,
|
|
5094
|
-
namedImports,
|
|
5095
|
-
importerFile: file,
|
|
5096
|
-
listenerRegistryFile,
|
|
5097
|
-
files,
|
|
5098
|
-
})
|
|
5099
|
-
) {
|
|
5270
|
+
if (referencesCentral) {
|
|
5100
5271
|
centralListenerRegistryReferenced = true;
|
|
5101
5272
|
}
|
|
5273
|
+
if (!hasSafeListenerRegistrationLifecycle(call, sourceFile)) {
|
|
5274
|
+
unsafeLifecycleFiles.add(file);
|
|
5275
|
+
}
|
|
5102
5276
|
}
|
|
5103
5277
|
|
|
5104
|
-
if (
|
|
5278
|
+
if (foundRelevantCall) {
|
|
5105
5279
|
wiringFile ??= file;
|
|
5106
5280
|
} else if (!eventBusFile && /\bcreate\w*EventBus\s*\(/.test(source)) {
|
|
5107
5281
|
eventBusFile = file;
|
|
@@ -5124,7 +5298,368 @@ async function listenerWiringReferences(
|
|
|
5124
5298
|
}
|
|
5125
5299
|
}
|
|
5126
5300
|
|
|
5127
|
-
return {
|
|
5301
|
+
return {
|
|
5302
|
+
identifiers,
|
|
5303
|
+
wiringFile,
|
|
5304
|
+
eventBusFile,
|
|
5305
|
+
unsafeLifecycleFiles: [...unsafeLifecycleFiles].sort(),
|
|
5306
|
+
};
|
|
5307
|
+
}
|
|
5308
|
+
|
|
5309
|
+
type ListenerLifecycleHook =
|
|
5310
|
+
| ts.MethodDeclaration
|
|
5311
|
+
| ts.FunctionExpression
|
|
5312
|
+
| ts.ArrowFunction;
|
|
5313
|
+
|
|
5314
|
+
interface ListenerLifecycleTarget {
|
|
5315
|
+
text: string;
|
|
5316
|
+
binding?: ts.Node;
|
|
5317
|
+
}
|
|
5318
|
+
|
|
5319
|
+
function registerListenersCalls(
|
|
5320
|
+
sourceFile: ts.SourceFile,
|
|
5321
|
+
bindings: ReadonlySet<string>,
|
|
5322
|
+
): ts.CallExpression[] {
|
|
5323
|
+
const calls: ts.CallExpression[] = [];
|
|
5324
|
+
const visit = (node: ts.Node): void => {
|
|
5325
|
+
if (
|
|
5326
|
+
ts.isCallExpression(node) &&
|
|
5327
|
+
((ts.isIdentifier(node.expression) &&
|
|
5328
|
+
bindings.has(node.expression.text)) ||
|
|
5329
|
+
(ts.isPropertyAccessExpression(node.expression) &&
|
|
5330
|
+
node.expression.name.text === "registerListeners" &&
|
|
5331
|
+
ts.isIdentifier(node.expression.expression) &&
|
|
5332
|
+
bindings.has(`${node.expression.expression.text}.*`)))
|
|
5333
|
+
) {
|
|
5334
|
+
calls.push(node);
|
|
5335
|
+
}
|
|
5336
|
+
ts.forEachChild(node, visit);
|
|
5337
|
+
};
|
|
5338
|
+
visit(sourceFile);
|
|
5339
|
+
return calls;
|
|
5340
|
+
}
|
|
5341
|
+
|
|
5342
|
+
function importedRegisterListenersBindings(
|
|
5343
|
+
source: string,
|
|
5344
|
+
namedImports: Map<string, { importedName: string; sourcePath: string }>,
|
|
5345
|
+
): Set<string> {
|
|
5346
|
+
const bindings = new Set<string>();
|
|
5347
|
+
for (const [localName, imported] of namedImports) {
|
|
5348
|
+
if (
|
|
5349
|
+
imported.importedName === "registerListeners" &&
|
|
5350
|
+
imported.sourcePath === "@beignet/core/events"
|
|
5351
|
+
) {
|
|
5352
|
+
bindings.add(localName);
|
|
5353
|
+
}
|
|
5354
|
+
}
|
|
5355
|
+
const namespaceImport =
|
|
5356
|
+
/import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+["']@beignet\/core\/events["']/g;
|
|
5357
|
+
for (const match of source.matchAll(namespaceImport)) {
|
|
5358
|
+
bindings.add(`${match[1]}.*`);
|
|
5359
|
+
}
|
|
5360
|
+
return bindings;
|
|
5361
|
+
}
|
|
5362
|
+
|
|
5363
|
+
function hasSafeListenerRegistrationLifecycle(
|
|
5364
|
+
call: ts.CallExpression,
|
|
5365
|
+
sourceFile: ts.SourceFile,
|
|
5366
|
+
): boolean {
|
|
5367
|
+
const startHook = enclosingListenerLifecycleHook(call, "start");
|
|
5368
|
+
const target = listenerRegistrationTarget(call, sourceFile);
|
|
5369
|
+
if (!startHook || !target) return false;
|
|
5370
|
+
const lifecycleOwner = listenerLifecycleOwner(startHook);
|
|
5371
|
+
if (!lifecycleOwner) return false;
|
|
5372
|
+
|
|
5373
|
+
let observesReadiness = false;
|
|
5374
|
+
let observesCleanup = false;
|
|
5375
|
+
const visit = (node: ts.Node): void => {
|
|
5376
|
+
if (
|
|
5377
|
+
!observesReadiness &&
|
|
5378
|
+
ts.isPropertyAccessExpression(node) &&
|
|
5379
|
+
node.name.text === "ready" &&
|
|
5380
|
+
listenerLifecycleTargetsMatch(
|
|
5381
|
+
listenerLifecycleTarget(node.expression, sourceFile),
|
|
5382
|
+
target,
|
|
5383
|
+
) &&
|
|
5384
|
+
enclosingListenerLifecycleHook(node, "start") === startHook &&
|
|
5385
|
+
listenerLifecyclePromiseIsObserved(node, startHook)
|
|
5386
|
+
) {
|
|
5387
|
+
observesReadiness = true;
|
|
5388
|
+
}
|
|
5389
|
+
|
|
5390
|
+
if (
|
|
5391
|
+
!observesCleanup &&
|
|
5392
|
+
ts.isCallExpression(node) &&
|
|
5393
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
5394
|
+
node.expression.name.text === "unsubscribe" &&
|
|
5395
|
+
listenerLifecycleTargetsMatch(
|
|
5396
|
+
listenerLifecycleTarget(node.expression.expression, sourceFile),
|
|
5397
|
+
target,
|
|
5398
|
+
)
|
|
5399
|
+
) {
|
|
5400
|
+
const stopHook = enclosingListenerLifecycleHook(node, "stop");
|
|
5401
|
+
if (
|
|
5402
|
+
stopHook &&
|
|
5403
|
+
listenerLifecycleOwner(stopHook) === lifecycleOwner &&
|
|
5404
|
+
listenerLifecyclePromiseIsObserved(node, stopHook)
|
|
5405
|
+
) {
|
|
5406
|
+
observesCleanup = true;
|
|
5407
|
+
}
|
|
5408
|
+
}
|
|
5409
|
+
|
|
5410
|
+
if (!observesReadiness || !observesCleanup) {
|
|
5411
|
+
ts.forEachChild(node, visit);
|
|
5412
|
+
}
|
|
5413
|
+
};
|
|
5414
|
+
visit(lifecycleOwner);
|
|
5415
|
+
return observesReadiness && observesCleanup;
|
|
5416
|
+
}
|
|
5417
|
+
|
|
5418
|
+
function listenerRegistrationTarget(
|
|
5419
|
+
call: ts.CallExpression,
|
|
5420
|
+
sourceFile: ts.SourceFile,
|
|
5421
|
+
): ListenerLifecycleTarget | undefined {
|
|
5422
|
+
let expression: ts.Expression = call;
|
|
5423
|
+
while (
|
|
5424
|
+
ts.isParenthesizedExpression(expression.parent) ||
|
|
5425
|
+
ts.isAsExpression(expression.parent) ||
|
|
5426
|
+
ts.isSatisfiesExpression(expression.parent) ||
|
|
5427
|
+
ts.isNonNullExpression(expression.parent)
|
|
5428
|
+
) {
|
|
5429
|
+
expression = expression.parent;
|
|
5430
|
+
}
|
|
5431
|
+
|
|
5432
|
+
const parent = expression.parent;
|
|
5433
|
+
if (
|
|
5434
|
+
ts.isVariableDeclaration(parent) &&
|
|
5435
|
+
parent.initializer === expression &&
|
|
5436
|
+
ts.isIdentifier(parent.name)
|
|
5437
|
+
) {
|
|
5438
|
+
return { text: parent.name.text, binding: parent };
|
|
5439
|
+
}
|
|
5440
|
+
if (
|
|
5441
|
+
ts.isBinaryExpression(parent) &&
|
|
5442
|
+
parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
5443
|
+
parent.right === expression
|
|
5444
|
+
) {
|
|
5445
|
+
return listenerLifecycleTarget(parent.left, sourceFile);
|
|
5446
|
+
}
|
|
5447
|
+
return undefined;
|
|
5448
|
+
}
|
|
5449
|
+
|
|
5450
|
+
function listenerLifecycleTarget(
|
|
5451
|
+
expression: ts.Expression,
|
|
5452
|
+
sourceFile: ts.SourceFile,
|
|
5453
|
+
): ListenerLifecycleTarget | undefined {
|
|
5454
|
+
let current = expression;
|
|
5455
|
+
while (
|
|
5456
|
+
ts.isParenthesizedExpression(current) ||
|
|
5457
|
+
ts.isAsExpression(current) ||
|
|
5458
|
+
ts.isSatisfiesExpression(current) ||
|
|
5459
|
+
ts.isNonNullExpression(current)
|
|
5460
|
+
) {
|
|
5461
|
+
current = current.expression;
|
|
5462
|
+
}
|
|
5463
|
+
if (!ts.isIdentifier(current) && !ts.isPropertyAccessExpression(current)) {
|
|
5464
|
+
return undefined;
|
|
5465
|
+
}
|
|
5466
|
+
|
|
5467
|
+
return {
|
|
5468
|
+
text: current.getText(sourceFile),
|
|
5469
|
+
binding: listenerLifecycleRootIdentifier(current)
|
|
5470
|
+
? resolveListenerLifecycleBinding(
|
|
5471
|
+
listenerLifecycleRootIdentifier(current) as ts.Identifier,
|
|
5472
|
+
)
|
|
5473
|
+
: undefined,
|
|
5474
|
+
};
|
|
5475
|
+
}
|
|
5476
|
+
|
|
5477
|
+
function listenerLifecycleRootIdentifier(
|
|
5478
|
+
expression: ts.Identifier | ts.PropertyAccessExpression,
|
|
5479
|
+
): ts.Identifier | undefined {
|
|
5480
|
+
let current: ts.Expression = expression;
|
|
5481
|
+
while (ts.isPropertyAccessExpression(current)) {
|
|
5482
|
+
current = current.expression;
|
|
5483
|
+
}
|
|
5484
|
+
return ts.isIdentifier(current) ? current : undefined;
|
|
5485
|
+
}
|
|
5486
|
+
|
|
5487
|
+
function resolveListenerLifecycleBinding(
|
|
5488
|
+
identifier: ts.Identifier,
|
|
5489
|
+
): ts.Node | undefined {
|
|
5490
|
+
let current: ts.Node | undefined = identifier;
|
|
5491
|
+
while (current) {
|
|
5492
|
+
if (ts.isBlock(current) || ts.isSourceFile(current)) {
|
|
5493
|
+
for (const statement of current.statements) {
|
|
5494
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
5495
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
5496
|
+
if (
|
|
5497
|
+
ts.isIdentifier(declaration.name) &&
|
|
5498
|
+
declaration.name.text === identifier.text
|
|
5499
|
+
) {
|
|
5500
|
+
return declaration;
|
|
5501
|
+
}
|
|
5502
|
+
}
|
|
5503
|
+
}
|
|
5504
|
+
}
|
|
5505
|
+
|
|
5506
|
+
if (ts.isFunctionLike(current)) {
|
|
5507
|
+
for (const parameter of current.parameters) {
|
|
5508
|
+
if (
|
|
5509
|
+
ts.isIdentifier(parameter.name) &&
|
|
5510
|
+
parameter.name.text === identifier.text
|
|
5511
|
+
) {
|
|
5512
|
+
return parameter;
|
|
5513
|
+
}
|
|
5514
|
+
}
|
|
5515
|
+
}
|
|
5516
|
+
|
|
5517
|
+
if (
|
|
5518
|
+
ts.isCatchClause(current) &&
|
|
5519
|
+
current.variableDeclaration &&
|
|
5520
|
+
ts.isIdentifier(current.variableDeclaration.name) &&
|
|
5521
|
+
current.variableDeclaration.name.text === identifier.text
|
|
5522
|
+
) {
|
|
5523
|
+
return current.variableDeclaration;
|
|
5524
|
+
}
|
|
5525
|
+
current = current.parent;
|
|
5526
|
+
}
|
|
5527
|
+
return undefined;
|
|
5528
|
+
}
|
|
5529
|
+
|
|
5530
|
+
function listenerLifecycleTargetsMatch(
|
|
5531
|
+
left: ListenerLifecycleTarget | undefined,
|
|
5532
|
+
right: ListenerLifecycleTarget,
|
|
5533
|
+
): boolean {
|
|
5534
|
+
return (
|
|
5535
|
+
left?.text === right.text &&
|
|
5536
|
+
(left.binding !== undefined || right.binding !== undefined
|
|
5537
|
+
? left.binding === right.binding
|
|
5538
|
+
: true)
|
|
5539
|
+
);
|
|
5540
|
+
}
|
|
5541
|
+
|
|
5542
|
+
function enclosingListenerLifecycleHook(
|
|
5543
|
+
node: ts.Node,
|
|
5544
|
+
hookName: "start" | "stop",
|
|
5545
|
+
): ListenerLifecycleHook | undefined {
|
|
5546
|
+
let current: ts.Node | undefined = node.parent;
|
|
5547
|
+
while (current) {
|
|
5548
|
+
if (ts.isMethodDeclaration(current)) {
|
|
5549
|
+
return staticPropertyName(current.name) === hookName
|
|
5550
|
+
? current
|
|
5551
|
+
: undefined;
|
|
5552
|
+
}
|
|
5553
|
+
if (ts.isFunctionExpression(current) || ts.isArrowFunction(current)) {
|
|
5554
|
+
const parent = current.parent;
|
|
5555
|
+
return ts.isPropertyAssignment(parent) &&
|
|
5556
|
+
staticPropertyName(parent.name) === hookName
|
|
5557
|
+
? current
|
|
5558
|
+
: undefined;
|
|
5559
|
+
}
|
|
5560
|
+
if (ts.isFunctionDeclaration(current)) return undefined;
|
|
5561
|
+
current = current.parent;
|
|
5562
|
+
}
|
|
5563
|
+
return undefined;
|
|
5564
|
+
}
|
|
5565
|
+
|
|
5566
|
+
function listenerLifecycleOwner(
|
|
5567
|
+
hook: ListenerLifecycleHook,
|
|
5568
|
+
): ts.ObjectLiteralExpression | undefined {
|
|
5569
|
+
if (ts.isMethodDeclaration(hook)) {
|
|
5570
|
+
return ts.isObjectLiteralExpression(hook.parent) ? hook.parent : undefined;
|
|
5571
|
+
}
|
|
5572
|
+
const property = hook.parent;
|
|
5573
|
+
return ts.isPropertyAssignment(property) &&
|
|
5574
|
+
ts.isObjectLiteralExpression(property.parent)
|
|
5575
|
+
? property.parent
|
|
5576
|
+
: undefined;
|
|
5577
|
+
}
|
|
5578
|
+
|
|
5579
|
+
function listenerLifecyclePromiseIsObserved(
|
|
5580
|
+
node: ts.Node,
|
|
5581
|
+
hook: ListenerLifecycleHook,
|
|
5582
|
+
): boolean {
|
|
5583
|
+
let current: ts.Node | undefined = node;
|
|
5584
|
+
while (current && current !== hook) {
|
|
5585
|
+
const parent: ts.Node = current.parent;
|
|
5586
|
+
if (
|
|
5587
|
+
(ts.isAwaitExpression(parent) && parent.expression === current) ||
|
|
5588
|
+
(ts.isReturnStatement(parent) && parent.expression === current)
|
|
5589
|
+
) {
|
|
5590
|
+
return true;
|
|
5591
|
+
}
|
|
5592
|
+
if (
|
|
5593
|
+
ts.isParenthesizedExpression(parent) ||
|
|
5594
|
+
ts.isAsExpression(parent) ||
|
|
5595
|
+
ts.isSatisfiesExpression(parent) ||
|
|
5596
|
+
ts.isNonNullExpression(parent)
|
|
5597
|
+
) {
|
|
5598
|
+
current = parent;
|
|
5599
|
+
continue;
|
|
5600
|
+
}
|
|
5601
|
+
if (
|
|
5602
|
+
ts.isPropertyAccessExpression(parent) &&
|
|
5603
|
+
parent.expression === current
|
|
5604
|
+
) {
|
|
5605
|
+
const method = parent.name.text;
|
|
5606
|
+
const invocation = parent.parent;
|
|
5607
|
+
if (
|
|
5608
|
+
!["then", "catch", "finally"].includes(method) ||
|
|
5609
|
+
!ts.isCallExpression(invocation) ||
|
|
5610
|
+
invocation.expression !== parent ||
|
|
5611
|
+
method === "catch" ||
|
|
5612
|
+
(method === "then" && invocation.arguments.length > 1)
|
|
5613
|
+
) {
|
|
5614
|
+
return false;
|
|
5615
|
+
}
|
|
5616
|
+
current = invocation;
|
|
5617
|
+
continue;
|
|
5618
|
+
}
|
|
5619
|
+
return false;
|
|
5620
|
+
}
|
|
5621
|
+
return false;
|
|
5622
|
+
}
|
|
5623
|
+
|
|
5624
|
+
function callReferencesFeatureListenerRegistry(args: {
|
|
5625
|
+
callIdentifiers: Set<string>;
|
|
5626
|
+
namedImports: Map<string, { importedName: string; sourcePath: string }>;
|
|
5627
|
+
importerFile: string;
|
|
5628
|
+
files: string[];
|
|
5629
|
+
listenerRegistries: FeatureWorkflowRegistry[];
|
|
5630
|
+
}): boolean {
|
|
5631
|
+
for (const identifier of args.callIdentifiers) {
|
|
5632
|
+
for (const registry of args.listenerRegistries) {
|
|
5633
|
+
if (
|
|
5634
|
+
identifier === registry.registryName ||
|
|
5635
|
+
registry.members.includes(identifier)
|
|
5636
|
+
) {
|
|
5637
|
+
return true;
|
|
5638
|
+
}
|
|
5639
|
+
}
|
|
5640
|
+
|
|
5641
|
+
const imported = args.namedImports.get(identifier);
|
|
5642
|
+
if (!imported) continue;
|
|
5643
|
+
const importedFile = sourceFileFromImport(
|
|
5644
|
+
imported.sourcePath,
|
|
5645
|
+
args.importerFile,
|
|
5646
|
+
args.files,
|
|
5647
|
+
);
|
|
5648
|
+
if (!importedFile) continue;
|
|
5649
|
+
|
|
5650
|
+
for (const registry of args.listenerRegistries) {
|
|
5651
|
+
if (
|
|
5652
|
+
(imported.importedName === registry.registryName &&
|
|
5653
|
+
importedFile === registry.indexFile) ||
|
|
5654
|
+
(registry.members.includes(imported.importedName) &&
|
|
5655
|
+
(importedFile === registry.indexFile ||
|
|
5656
|
+
registry.memberFiles.get(imported.importedName) === importedFile))
|
|
5657
|
+
) {
|
|
5658
|
+
return true;
|
|
5659
|
+
}
|
|
5660
|
+
}
|
|
5661
|
+
}
|
|
5662
|
+
return false;
|
|
5128
5663
|
}
|
|
5129
5664
|
|
|
5130
5665
|
function callReferencesCentralListenerRegistry(args: {
|