@webpresso/agent-config 0.4.2 → 0.4.4

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.
@@ -0,0 +1,220 @@
1
+ // @ts-nocheck
2
+ // Webpresso import hygiene rules — replaces GritQL patterns:
3
+ // - no-relative-parent-imports
4
+ // - no-src-path-imports
5
+ // - no-relative-mock-paths
6
+ // - no-forbidden-package-imports
7
+ import { isAgentKitSeaTreeFile, isPlatformAppFile, isSdkPackageFile, normalizeFilename, } from "./path-roles.js";
8
+ export { isAgentKitSeaTreeFile, isPlatformAppFile, isSdkPackageFile, normalizeFilename, } from "./path-roles.js";
9
+ const CROSS_PACKAGE_SEGMENT = /(?:^|\/)(packages|apps|tooling|infra|webpresso)\//;
10
+ function isRelativeParentPath(source) {
11
+ return typeof source === "string" && source.includes("../");
12
+ }
13
+ function isRelativeGeneratedImport(source) {
14
+ return (typeof source === "string" && source.startsWith(".") && source.includes(".webpresso/generated/"));
15
+ }
16
+ function isGeneratedPathsSourceImport(source) {
17
+ return (typeof source === "string" && source.includes("/packages/cli/cli-utils/src/generated-paths"));
18
+ }
19
+ function isAllowedRelativeParentImport(source) {
20
+ return isGeneratedPathsSourceImport(source);
21
+ }
22
+ function isCrossPackageRelativePath(source) {
23
+ return isRelativeParentPath(source) && CROSS_PACKAGE_SEGMENT.test(source);
24
+ }
25
+ function reportRelativeParentImport(context, node) {
26
+ context.report({
27
+ node,
28
+ message: "Use `#` or package imports instead of relative parent imports. For cross-package access, add a proper package export and import via the package name.",
29
+ });
30
+ }
31
+ function reportCrossPackageImport(context, node) {
32
+ context.report({
33
+ node,
34
+ message: "Do not reach into another package/app with a relative filesystem import. Add a proper package export and import it via the package name.",
35
+ });
36
+ }
37
+ function getFilename(context) {
38
+ if (typeof context.getFilename === "function") {
39
+ return normalizeFilename(context.getFilename());
40
+ }
41
+ return normalizeFilename(context.filename);
42
+ }
43
+ function checkStaticModuleSource(node, callback) {
44
+ if (!node.source)
45
+ return;
46
+ callback(node.source.value, node.source);
47
+ }
48
+ /** Package imports forbidden in agent-kit SEA surfaces (schema + ui paint kits). */
49
+ export function isSeaForbiddenPackageImport(source) {
50
+ if (typeof source !== "string")
51
+ return false;
52
+ return (source === "@webpresso/schema" ||
53
+ source.startsWith("@webpresso/schema/") ||
54
+ source === "@webpresso/ui" ||
55
+ source.startsWith("@webpresso/ui/"));
56
+ }
57
+ export const SEA_FORBIDDEN_IMPORT_MESSAGE = "Agent-kit SEA surfaces (compose / wp_ui_*) must not import `@webpresso/schema` or `@webpresso/ui`. Use the two-tier block registry snapshot/project file and local Zod/HTML paint instead (offline / nondeterminism lock).";
58
+ function isSchemaEngineImport(source) {
59
+ return typeof source === "string" && source.startsWith("@webpresso/schema/engine/");
60
+ }
61
+ function isSchemaRuntimeImport(source) {
62
+ return typeof source === "string" && source.startsWith("@webpresso/schema-runtime");
63
+ }
64
+ function isGeneratedImport(source) {
65
+ return typeof source === "string" && source.startsWith("@webpresso/generated/");
66
+ }
67
+ const FORBIDDEN_SDK_TOOLING_IMPORTS = new Set([
68
+ "@webpresso/schema/loaders",
69
+ "@webpresso/cli-utils",
70
+ "@webpresso/blueprint",
71
+ ]);
72
+ const noRelativeParentImports = {
73
+ create(context) {
74
+ function checkSource(source, node) {
75
+ if (isRelativeGeneratedImport(source)) {
76
+ context.report({
77
+ node,
78
+ message: "Do not import generated artifacts via relative filesystem paths. Use the real workspace package `@webpresso/generated/*` instead.",
79
+ });
80
+ return;
81
+ }
82
+ if (isRelativeParentPath(source) && !isAllowedRelativeParentImport(source)) {
83
+ reportRelativeParentImport(context, node);
84
+ }
85
+ }
86
+ return {
87
+ ImportDeclaration(node) {
88
+ checkSource(node.source.value, node.source);
89
+ },
90
+ // Also catch export ... from '../...'
91
+ ExportNamedDeclaration(node) {
92
+ checkStaticModuleSource(node, checkSource);
93
+ },
94
+ ExportAllDeclaration(node) {
95
+ checkStaticModuleSource(node, checkSource);
96
+ },
97
+ ImportExpression(node) {
98
+ if (node.source.type !== "Literal")
99
+ return;
100
+ const source = node.source.value;
101
+ if (isCrossPackageRelativePath(source)) {
102
+ reportCrossPackageImport(context, node.source);
103
+ }
104
+ },
105
+ };
106
+ },
107
+ };
108
+ const noSrcPathImports = {
109
+ create(context) {
110
+ return {
111
+ ImportDeclaration(node) {
112
+ const source = node.source.value;
113
+ if (typeof source === "string" && source.includes("../src/")) {
114
+ context.report({
115
+ node: node.source,
116
+ message: "Use `#` alias instead of `../src/` paths (e.g. '#database' not '../src/database').",
117
+ });
118
+ }
119
+ },
120
+ };
121
+ },
122
+ };
123
+ const noRelativeMockPaths = {
124
+ create(context) {
125
+ return {
126
+ CallExpression(node) {
127
+ if (node.callee.type !== "MemberExpression" ||
128
+ node.callee.object.type !== "Identifier" ||
129
+ node.callee.object.name !== "vi" ||
130
+ node.callee.property.type !== "Identifier" ||
131
+ node.callee.property.name !== "mock") {
132
+ return;
133
+ }
134
+ const arg = node.arguments[0];
135
+ if (!arg || arg.type !== "Literal" || typeof arg.value !== "string")
136
+ return;
137
+ if (arg.value.includes("../")) {
138
+ context.report({
139
+ node: arg,
140
+ message: "Ban vi.mock() with relative parent paths. Use the module's package name or `#` alias instead.",
141
+ });
142
+ }
143
+ },
144
+ };
145
+ },
146
+ };
147
+ const noForbiddenPackageImports = {
148
+ create(context) {
149
+ const filename = getFilename(context);
150
+ function checkImport(source, node) {
151
+ if (typeof source !== "string")
152
+ return;
153
+ // Agent-kit SEA / offline compose — ban before monorepo platform allow rules.
154
+ // Consumers still get this rule via shipped oxlintrc; monorepo apps do not match
155
+ // isAgentKitSeaTreeFile so public schema imports remain allowed there.
156
+ if (isAgentKitSeaTreeFile(filename) && isSeaForbiddenPackageImport(source)) {
157
+ context.report({
158
+ node,
159
+ message: SEA_FORBIDDEN_IMPORT_MESSAGE,
160
+ });
161
+ return;
162
+ }
163
+ if (isPlatformAppFile(filename) &&
164
+ isSchemaEngineImport(source) &&
165
+ source !== "@webpresso/schema/engine/api") {
166
+ context.report({
167
+ node,
168
+ message: "Platform apps may only import `@webpresso/schema/engine/api`, `@webpresso/generated/*`, or `@webpresso/schema-runtime/*`. Move schema-engine internals behind a public surface.",
169
+ });
170
+ return;
171
+ }
172
+ if (isPlatformAppFile(filename) && isGeneratedImport(source)) {
173
+ return;
174
+ }
175
+ if (isSdkPackageFile(filename) && FORBIDDEN_SDK_TOOLING_IMPORTS.has(source)) {
176
+ context.report({
177
+ node,
178
+ message: "SDK packages must not import CLI/tooling packages such as `@webpresso/schema/loaders`, `@webpresso/cli-utils`, or `@webpresso/blueprint`.",
179
+ });
180
+ return;
181
+ }
182
+ // Runtime helpers are allowed for platform apps, so keep the explicit allowlist
183
+ // visible here rather than indirectly relying on a broader schema-engine prefix.
184
+ if (isPlatformAppFile(filename) &&
185
+ isSchemaRuntimeImport(source) &&
186
+ !source.startsWith("@webpresso/schema-runtime/")) {
187
+ context.report({
188
+ node,
189
+ message: "Platform apps must import `@webpresso/schema-runtime/*` subpaths, not the package root.",
190
+ });
191
+ }
192
+ }
193
+ return {
194
+ ImportDeclaration(node) {
195
+ checkImport(node.source.value, node.source);
196
+ },
197
+ ExportNamedDeclaration(node) {
198
+ checkStaticModuleSource(node, checkImport);
199
+ },
200
+ ExportAllDeclaration(node) {
201
+ checkStaticModuleSource(node, checkImport);
202
+ },
203
+ ImportExpression(node) {
204
+ if (node.source.type !== "Literal")
205
+ return;
206
+ checkImport(node.source.value, node.source);
207
+ },
208
+ };
209
+ },
210
+ };
211
+ const plugin = {
212
+ meta: { name: "webpresso-imports" },
213
+ rules: {
214
+ "no-relative-parent-imports": noRelativeParentImports,
215
+ "no-src-path-imports": noSrcPathImports,
216
+ "no-relative-mock-paths": noRelativeMockPaths,
217
+ "no-forbidden-package-imports": noForbiddenPackageImports,
218
+ },
219
+ };
220
+ export default plugin;
@@ -0,0 +1,23 @@
1
+ import codeSafety from "./code-safety.js";
2
+ import foundationPurity from "./foundation-purity.js";
3
+ import graphqlConventions from "./graphql-conventions.js";
4
+ import importHygiene from "./import-hygiene.js";
5
+ import monorepoNpaths from "./monorepo-paths.js";
6
+ import queryPatterns from "./query-patterns.js";
7
+ import testingQuality from "./testing-quality.js";
8
+ import tierBoundaries from "./tier-boundaries.js";
9
+ export { OXLINT_PACKAGE_SUBPATH_BASENAMES, OXLINT_PATH_ROLES_BASENAME, OXLINT_POLICY_PLUGIN_BASENAMES, agentConfigOxlintPluginSpecifier, agentConfigOxlintPluginSpecifiers, type OxlintPackageSubpathBasename, type OxlintPathRolesBasename, type OxlintPolicyPluginBasename, } from "./basenames.js";
10
+ type OxlintRuleSeverity = "error";
11
+ interface OxlintPlugin {
12
+ meta: {
13
+ name: string;
14
+ };
15
+ rules: Record<string, unknown>;
16
+ }
17
+ export { codeSafety, foundationPurity, graphqlConventions, importHygiene, monorepoNpaths, queryPatterns, testingQuality, tierBoundaries, };
18
+ export declare const plugins: Record<string, OxlintPlugin>;
19
+ export declare const rules: Record<string, OxlintRuleSeverity>;
20
+ export declare const config: {
21
+ plugins: Record<string, OxlintPlugin>;
22
+ rules: Record<string, "error">;
23
+ };
@@ -0,0 +1,29 @@
1
+ import codeSafety from "./code-safety.js";
2
+ import foundationPurity from "./foundation-purity.js";
3
+ import graphqlConventions from "./graphql-conventions.js";
4
+ import importHygiene from "./import-hygiene.js";
5
+ import monorepoNpaths from "./monorepo-paths.js";
6
+ import queryPatterns from "./query-patterns.js";
7
+ import testingQuality from "./testing-quality.js";
8
+ import tierBoundaries from "./tier-boundaries.js";
9
+ export { OXLINT_PACKAGE_SUBPATH_BASENAMES, OXLINT_PATH_ROLES_BASENAME, OXLINT_POLICY_PLUGIN_BASENAMES, agentConfigOxlintPluginSpecifier, agentConfigOxlintPluginSpecifiers, } from "./basenames.js";
10
+ export { codeSafety, foundationPurity, graphqlConventions, importHygiene, monorepoNpaths, queryPatterns, testingQuality, tierBoundaries, };
11
+ const pluginEntries = [
12
+ codeSafety,
13
+ foundationPurity,
14
+ graphqlConventions,
15
+ importHygiene,
16
+ monorepoNpaths,
17
+ queryPatterns,
18
+ testingQuality,
19
+ tierBoundaries,
20
+ ].map((plugin) => [plugin.meta.name, plugin]);
21
+ export const plugins = Object.fromEntries(pluginEntries);
22
+ export const rules = Object.fromEntries(pluginEntries.flatMap(([pluginName, plugin]) => Object.keys(plugin.rules).map((ruleName) => [
23
+ `${pluginName}/${ruleName}`,
24
+ "error",
25
+ ])));
26
+ export const config = {
27
+ plugins,
28
+ rules,
29
+ };
@@ -0,0 +1,22 @@
1
+ export declare function hardcodedRepoRootDepth(args: any): {
2
+ depth: number;
3
+ index: number;
4
+ } | null;
5
+ declare const plugin: {
6
+ meta: {
7
+ name: string;
8
+ };
9
+ rules: {
10
+ "no-hardcoded-repo-root": {
11
+ create(context: any): {
12
+ CallExpression(node: any): void;
13
+ };
14
+ };
15
+ "no-cross-package-paths": {
16
+ create(context: any): {
17
+ CallExpression(node: any): void;
18
+ };
19
+ };
20
+ };
21
+ };
22
+ export default plugin;
@@ -0,0 +1,135 @@
1
+ // @ts-nocheck
2
+ // Webpresso monorepo path rules — replaces GritQL patterns:
3
+ // - no-hardcoded-repo-root (import.meta.dirname + '../..')
4
+ // - no-hardcoded-repo-root-dirname (__dirname + '../..')
5
+ // - no-cross-package-paths (single-arg)
6
+ // - no-cross-package-paths-multiarg
7
+ const RESOLVE_FUNCTIONS = new Set(["resolve", "join"]);
8
+ const QUALIFIED_RESOLVE = new Set(["path.resolve", "path.join"]);
9
+ function getCalleeName(callee) {
10
+ if (callee.type === "Identifier")
11
+ return callee.name;
12
+ if (callee.type === "MemberExpression" &&
13
+ callee.object.type === "Identifier" &&
14
+ callee.property.type === "Identifier") {
15
+ return `${callee.object.name}.${callee.property.name}`;
16
+ }
17
+ return null;
18
+ }
19
+ function isMetaDirname(node) {
20
+ return (node.type === "MemberExpression" &&
21
+ node.object.type === "MetaProperty" &&
22
+ node.property.type === "Identifier" &&
23
+ node.property.name === "dirname");
24
+ }
25
+ function isDunderDirname(node) {
26
+ return node.type === "Identifier" && node.name === "__dirname";
27
+ }
28
+ // Count parent-directory ('..') segments in a single path-literal value.
29
+ // Handles a combined literal ('../..' → 2) and both path separators.
30
+ function countParentSegments(value) {
31
+ if (typeof value !== "string")
32
+ return 0;
33
+ return value.split(/[/\\]/u).filter((segment) => segment === "..").length;
34
+ }
35
+ // Total '..' traversal depth across the resolve/join arguments after the
36
+ // anchor. Returns { depth, index } pointing at the first traversal argument
37
+ // when depth >= 2 (the repo-root-climb threshold), else null.
38
+ //
39
+ // Unifying the single-literal ('../..') and multi-argument ('..', '..') forms
40
+ // into one depth count closes a gap: the previous matcher only caught the
41
+ // multi-argument form at 4+ arguments, so `join(import.meta.dirname, '..',
42
+ // '..')` (two separate '..' args, depth 2) silently passed.
43
+ export function hardcodedRepoRootDepth(args) {
44
+ let depth = 0;
45
+ let index = -1;
46
+ for (let i = 1; i < args.length; i += 1) {
47
+ const arg = args[i];
48
+ if (arg.type !== "Literal")
49
+ continue;
50
+ const segments = countParentSegments(arg.value);
51
+ if (segments > 0 && index === -1)
52
+ index = i;
53
+ depth += segments;
54
+ }
55
+ return depth >= 2 && index !== -1 ? { depth, index } : null;
56
+ }
57
+ const noHardcodedRepoRoot = {
58
+ create(context) {
59
+ return {
60
+ CallExpression(node) {
61
+ const name = getCalleeName(node.callee);
62
+ if (!name)
63
+ return;
64
+ if (!RESOLVE_FUNCTIONS.has(name) && !QUALIFIED_RESOLVE.has(name))
65
+ return;
66
+ const args = node.arguments;
67
+ if (args.length < 2)
68
+ return;
69
+ const violation = hardcodedRepoRootDepth(args);
70
+ if (!violation)
71
+ return;
72
+ const first = args[0];
73
+ const source = isMetaDirname(first)
74
+ ? "import.meta.dirname"
75
+ : isDunderDirname(first)
76
+ ? "__dirname"
77
+ : "variable";
78
+ context.report({
79
+ node: args[violation.index],
80
+ message: `Hardcoded repo root via ${source} + '..' traversal (depth ${violation.depth}). Derive the repo root dynamically — search upward for a workspace marker such as pnpm-workspace.yaml (e.g. findRepoRoot) — instead of hardcoding the directory depth.`,
81
+ });
82
+ },
83
+ };
84
+ },
85
+ };
86
+ const MONOREPO_DIRS = /(?:packages|apps|tooling|infra)/;
87
+ const noCrossPackagePaths = {
88
+ create(context) {
89
+ return {
90
+ CallExpression(node) {
91
+ const name = getCalleeName(node.callee);
92
+ if (!name)
93
+ return;
94
+ if (!RESOLVE_FUNCTIONS.has(name) && !QUALIFIED_RESOLVE.has(name))
95
+ return;
96
+ const args = node.arguments;
97
+ if (args.length < 2)
98
+ return;
99
+ const first = args[0];
100
+ if (!isDunderDirname(first) && !isMetaDirname(first))
101
+ return;
102
+ // Single-arg: resolve(__dirname, '../../../packages/foo')
103
+ for (let i = 1; i < args.length; i++) {
104
+ const arg = args[i];
105
+ if (arg.type !== "Literal" || typeof arg.value !== "string")
106
+ continue;
107
+ if (/\.\./.test(arg.value) && MONOREPO_DIRS.test(arg.value)) {
108
+ context.report({
109
+ node: arg,
110
+ message: "Cross-package path traversal via __dirname. Use proper package imports or findRepoRoot() instead.",
111
+ });
112
+ return;
113
+ }
114
+ }
115
+ // Multi-arg: resolve(__dirname, '..', '..', 'packages', 'cli2')
116
+ const hasParent = args.some((a) => a.type === "Literal" && a.value === "..");
117
+ const hasMonoDir = args.some((a) => a.type === "Literal" && typeof a.value === "string" && MONOREPO_DIRS.test(a.value));
118
+ if (hasParent && hasMonoDir) {
119
+ context.report({
120
+ node: args[1],
121
+ message: "Cross-package path traversal via __dirname. Use proper package imports or findRepoRoot() instead.",
122
+ });
123
+ }
124
+ },
125
+ };
126
+ },
127
+ };
128
+ const plugin = {
129
+ meta: { name: "webpresso-monorepo" },
130
+ rules: {
131
+ "no-hardcoded-repo-root": noHardcodedRepoRoot,
132
+ "no-cross-package-paths": noCrossPackagePaths,
133
+ },
134
+ };
135
+ export default plugin;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Shared path classifiers for webpresso oxlint policy plugins.
3
+ * Pure functions only — no AST. Consumers resolve plugins from
4
+ * `@webpresso/agent-config/oxlint/*`; these markers must match real trees.
5
+ */
6
+ /** Normalize Windows paths for substring role checks. */
7
+ export declare function normalizeFilename(filename: any): string;
8
+ /**
9
+ * Offline / SEA-shaped agent-kit surfaces (compose + UI MCP tools).
10
+ * Monorepo apps never use these path segments.
11
+ */
12
+ export declare function isAgentKitSeaTreeFile(filename: any): boolean;
13
+ /** Platform web apps: real monorepo layout + legacy `/apps/web/`. */
14
+ export declare function isPlatformWebFile(filename: any): boolean;
15
+ /** Platform workers: real monorepo layout + legacy `/apps/workers/`. */
16
+ export declare function isPlatformWorkerFile(filename: any): boolean;
17
+ /** Platform apps (web or worker) for import-hygiene schema-surface rules. */
18
+ export declare function isPlatformAppFile(filename: any): boolean;
19
+ /**
20
+ * Client UI surfaces for GraphQL/query-pattern rules.
21
+ * Real monorepo: apps/platform/web/.../app/; legacy apps/web/.../app/.
22
+ */
23
+ export declare function isWebAppSurface(filename: any): boolean;
24
+ export declare function isSdkPackageFile(filename: any): boolean;
@@ -0,0 +1,53 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * Shared path classifiers for webpresso oxlint policy plugins.
4
+ * Pure functions only — no AST. Consumers resolve plugins from
5
+ * `@webpresso/agent-config/oxlint/*`; these markers must match real trees.
6
+ */
7
+ /** Normalize Windows paths for substring role checks. */
8
+ export function normalizeFilename(filename) {
9
+ return typeof filename === "string" ? filename.replaceAll("\\", "/") : "";
10
+ }
11
+ /**
12
+ * Offline / SEA-shaped agent-kit surfaces (compose + UI MCP tools).
13
+ * Monorepo apps never use these path segments.
14
+ */
15
+ export function isAgentKitSeaTreeFile(filename) {
16
+ const n = normalizeFilename(filename);
17
+ if (n.includes("/node_modules/"))
18
+ return false;
19
+ return n.includes("/src/compose/") || n.includes("/src/mcp/tools/wp-ui-");
20
+ }
21
+ /** Platform web apps: real monorepo layout + legacy `/apps/web/`. */
22
+ export function isPlatformWebFile(filename) {
23
+ const n = normalizeFilename(filename);
24
+ if (n.includes("/node_modules/"))
25
+ return false;
26
+ return n.includes("/apps/platform/web/") || n.includes("/apps/web/");
27
+ }
28
+ /** Platform workers: real monorepo layout + legacy `/apps/workers/`. */
29
+ export function isPlatformWorkerFile(filename) {
30
+ const n = normalizeFilename(filename);
31
+ if (n.includes("/node_modules/"))
32
+ return false;
33
+ return n.includes("/apps/platform/workers/") || n.includes("/apps/workers/");
34
+ }
35
+ /** Platform apps (web or worker) for import-hygiene schema-surface rules. */
36
+ export function isPlatformAppFile(filename) {
37
+ return isPlatformWebFile(filename) || isPlatformWorkerFile(filename);
38
+ }
39
+ /**
40
+ * Client UI surfaces for GraphQL/query-pattern rules.
41
+ * Real monorepo: apps/platform/web/.../app/; legacy apps/web/.../app/.
42
+ */
43
+ export function isWebAppSurface(filename) {
44
+ const n = normalizeFilename(filename);
45
+ if (n.includes("/node_modules/"))
46
+ return false;
47
+ if (!n.includes("/app/"))
48
+ return false;
49
+ return n.includes("/apps/platform/web/") || n.includes("/apps/web/");
50
+ }
51
+ export function isSdkPackageFile(filename) {
52
+ return normalizeFilename(filename).includes("/packages/sdk/");
53
+ }
@@ -0,0 +1,24 @@
1
+ declare const plugin: {
2
+ meta: {
3
+ name: string;
4
+ };
5
+ rules: {
6
+ "no-adhoc-useQuery": {
7
+ create(context: any): {
8
+ CallExpression?: undefined;
9
+ } | {
10
+ CallExpression(node: any): void;
11
+ };
12
+ };
13
+ "no-isLoading-on-queries": {
14
+ create(context: any): {
15
+ VariableDeclarator?: undefined;
16
+ MemberExpression?: undefined;
17
+ } | {
18
+ VariableDeclarator(node: any): void;
19
+ MemberExpression(node: any): void;
20
+ };
21
+ };
22
+ };
23
+ };
24
+ export default plugin;
@@ -0,0 +1,122 @@
1
+ // @ts-nocheck
2
+ import { isWebAppSurface, normalizeFilename } from "./path-roles.js";
3
+ const QUERY_HOOK_NAMES = new Set(["useQuery", "useSuspenseQuery", "useInfiniteQuery"]);
4
+ function getFilename(context) {
5
+ if (typeof context.getFilename === "function") {
6
+ return normalizeFilename(context.getFilename());
7
+ }
8
+ return normalizeFilename(context.filename);
9
+ }
10
+ function isClientQuerySurface(filename) {
11
+ const normalized = normalizeFilename(filename);
12
+ if (!/\.tsx?$/.test(normalized))
13
+ return false;
14
+ if (normalized.includes("/.webpresso/generated/"))
15
+ return false;
16
+ const isFeatureSurface = normalized.includes("/packages/feature/") && normalized.includes("/src/");
17
+ return isWebAppSurface(normalized) || isFeatureSurface;
18
+ }
19
+ function getCalleeName(callee) {
20
+ if (!callee)
21
+ return null;
22
+ if (callee.type === "Identifier")
23
+ return callee.name;
24
+ if (callee.type === "MemberExpression" &&
25
+ !callee.computed &&
26
+ callee.property?.type === "Identifier") {
27
+ return callee.property.name;
28
+ }
29
+ return null;
30
+ }
31
+ function isQueryHookCall(node) {
32
+ return node?.type === "CallExpression" && QUERY_HOOK_NAMES.has(getCalleeName(node.callee));
33
+ }
34
+ function isTrackedQueryResult(init, queryResultBindings) {
35
+ return init?.type === "Identifier" && queryResultBindings.has(init.name);
36
+ }
37
+ function isIsLoadingProperty(property) {
38
+ return ((property?.type === "Identifier" && property.name === "isLoading") ||
39
+ (property?.type === "Literal" && property.value === "isLoading"));
40
+ }
41
+ function reportAdhocQuery(context, node) {
42
+ context.report({
43
+ node,
44
+ message: "TanStack Query hard cut: pass a named query options identifier to useQuery/useSuspenseQuery/useInfiniteQuery instead of an inline object literal.",
45
+ });
46
+ }
47
+ function reportIsLoading(context, node) {
48
+ context.report({
49
+ node,
50
+ message: "TanStack Query hard cut: use isPending (or domain-specific derived state) instead of isLoading on query results.",
51
+ });
52
+ }
53
+ const noAdhocUseQuery = {
54
+ create(context) {
55
+ if (!isClientQuerySurface(getFilename(context))) {
56
+ return {};
57
+ }
58
+ return {
59
+ CallExpression(node) {
60
+ if (!isQueryHookCall(node))
61
+ return;
62
+ const firstArgument = node.arguments[0];
63
+ if (firstArgument?.type !== "ObjectExpression")
64
+ return;
65
+ reportAdhocQuery(context, firstArgument);
66
+ },
67
+ };
68
+ },
69
+ };
70
+ const noIsLoadingOnQueries = {
71
+ create(context) {
72
+ if (!isClientQuerySurface(getFilename(context))) {
73
+ return {};
74
+ }
75
+ const queryResultBindings = new Set();
76
+ function trackQueryResultBinding(id, init) {
77
+ if (id?.type !== "Identifier")
78
+ return;
79
+ if (!isQueryHookCall(init))
80
+ return;
81
+ queryResultBindings.add(id.name);
82
+ }
83
+ function reportObjectPattern(node) {
84
+ for (const property of node.properties ?? []) {
85
+ if (property?.type !== "Property")
86
+ continue;
87
+ if (!isIsLoadingProperty(property.key))
88
+ continue;
89
+ reportIsLoading(context, property.key);
90
+ }
91
+ }
92
+ return {
93
+ VariableDeclarator(node) {
94
+ trackQueryResultBinding(node.id, node.init);
95
+ if (node.id?.type !== "ObjectPattern")
96
+ return;
97
+ if (!isQueryHookCall(node.init) && !isTrackedQueryResult(node.init, queryResultBindings))
98
+ return;
99
+ reportObjectPattern(node.id);
100
+ },
101
+ MemberExpression(node) {
102
+ if (node.computed)
103
+ return;
104
+ if (!isIsLoadingProperty(node.property))
105
+ return;
106
+ if (node.object?.type !== "Identifier")
107
+ return;
108
+ if (!queryResultBindings.has(node.object.name))
109
+ return;
110
+ reportIsLoading(context, node.property);
111
+ },
112
+ };
113
+ },
114
+ };
115
+ const plugin = {
116
+ meta: { name: "webpresso-query-patterns" },
117
+ rules: {
118
+ "no-adhoc-useQuery": noAdhocUseQuery,
119
+ "no-isLoading-on-queries": noIsLoadingOnQueries,
120
+ },
121
+ };
122
+ export default plugin;