@warlock.js/core 5.35.0 → 5.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/esm/dev-server/module-loader.mjs +1 -1
  3. package/esm/dev-server/parse-imports.mjs +1 -1
  4. package/esm/dev-server/type-generator.mjs +1 -1
  5. package/esm/e2e/cdp.d.mts.map +1 -1
  6. package/esm/e2e/cdp.mjs +9 -2
  7. package/esm/e2e/cdp.mjs.map +1 -1
  8. package/esm/e2e/page.d.mts +9 -1
  9. package/esm/e2e/page.d.mts.map +1 -1
  10. package/esm/e2e/page.mjs +45 -7
  11. package/esm/e2e/page.mjs.map +1 -1
  12. package/esm/e2e/types.d.mts +18 -0
  13. package/esm/e2e/types.d.mts.map +1 -1
  14. package/esm/e2e/types.mjs.map +1 -1
  15. package/esm/errors/error-codes.d.mts +24 -0
  16. package/esm/errors/error-codes.d.mts.map +1 -1
  17. package/esm/errors/error-codes.mjs +24 -0
  18. package/esm/errors/error-codes.mjs.map +1 -1
  19. package/esm/generations/features/test.feature.mjs +7 -7
  20. package/esm/generations/features/test.feature.mjs.map +1 -1
  21. package/esm/generations/features/web.feature.mjs +3 -3
  22. package/esm/generations/features/web.feature.mjs.map +1 -1
  23. package/esm/index.d.mts +4 -2
  24. package/esm/index.mjs +3 -1
  25. package/esm/localization/index.mjs +3 -1
  26. package/esm/localization/translation-usage-graph.d.mts +49 -0
  27. package/esm/localization/translation-usage-graph.d.mts.map +1 -0
  28. package/esm/localization/translation-usage-graph.mjs +128 -0
  29. package/esm/localization/translation-usage-graph.mjs.map +1 -0
  30. package/esm/localization/translation-usage.d.mts +85 -0
  31. package/esm/localization/translation-usage.d.mts.map +1 -0
  32. package/esm/localization/translation-usage.mjs +307 -0
  33. package/esm/localization/translation-usage.mjs.map +1 -0
  34. package/esm/production/build-budgets.d.mts +18 -0
  35. package/esm/production/build-budgets.d.mts.map +1 -1
  36. package/esm/production/build-budgets.mjs.map +1 -1
  37. package/esm/production/route-registration-snapshot.mjs +5 -3
  38. package/esm/production/route-registration-snapshot.mjs.map +1 -1
  39. package/esm/router/router.d.mts +2 -0
  40. package/esm/router/router.d.mts.map +1 -1
  41. package/esm/router/router.mjs +4 -1
  42. package/esm/router/router.mjs.map +1 -1
  43. package/esm/router/types.d.mts +19 -0
  44. package/esm/router/types.d.mts.map +1 -1
  45. package/esm/tooling/check/api-route-references.d.mts +16 -0
  46. package/esm/tooling/check/api-route-references.d.mts.map +1 -0
  47. package/esm/tooling/check/api-route-references.mjs +146 -0
  48. package/esm/tooling/check/api-route-references.mjs.map +1 -0
  49. package/esm/tooling/check/conventions-step.d.mts +6 -3
  50. package/esm/tooling/check/conventions-step.d.mts.map +1 -1
  51. package/esm/tooling/check/conventions-step.mjs +19 -10
  52. package/esm/tooling/check/conventions-step.mjs.map +1 -1
  53. package/esm/tooling/check/run-check.d.mts.map +1 -1
  54. package/esm/tooling/check/run-check.mjs +4 -2
  55. package/esm/tooling/check/run-check.mjs.map +1 -1
  56. package/esm/tooling/check/translation-conventions.mjs +32 -0
  57. package/esm/tooling/check/translation-conventions.mjs.map +1 -0
  58. package/esm/tooling/inspect/collect-files.mjs +1 -1
  59. package/esm/upgrade/codemods/index.mjs +2 -0
  60. package/esm/upgrade/codemods/index.mjs.map +1 -1
  61. package/esm/upgrade/codemods/vite-vitest.mjs +170 -0
  62. package/esm/upgrade/codemods/vite-vitest.mjs.map +1 -0
  63. package/esm/upgrade/notes.mjs +4 -0
  64. package/esm/upgrade/notes.mjs.map +1 -1
  65. package/esm/warlock-config/index.d.mts +2 -2
  66. package/llms-full.txt +127 -16
  67. package/package.json +13 -13
  68. package/skills/register-route/SKILL.md +13 -0
  69. package/skills/test-service/SKILL.md +8 -8
  70. package/skills/upgrade-from-5-29/SKILL.md +91 -0
  71. package/skills/use-localization/SKILL.md +4 -0
  72. package/skills/warlock-check/SKILL.md +11 -8
@@ -0,0 +1,16 @@
1
+ import "../syntax/ast.mjs";
2
+ import "./source-ast.mjs";
3
+ //#region ../../packages/core/src/tooling/check/api-route-references.d.ts
4
+ /**
5
+ * Project-wide facts the browser-reference checks need. Gathered once per run
6
+ * (`runConventions`) because a route is registered in one file and referenced
7
+ * from another.
8
+ */
9
+ export type ApiRouteFacts = {
10
+ /** Names of routes registered with `clientVisible: false`. */
11
+ readonly clientHiddenNames: ReadonlySet<string>;
12
+ /** Whether any page, layout or root setup file declares `config.apiRoutes`. */
13
+ readonly declaresApiRoutes: boolean;
14
+ };
15
+ //#endregion
16
+ //# sourceMappingURL=api-route-references.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-route-references.d.mts","names":[],"sources":["../../../../../../../../../packages/core/src/tooling/check/api-route-references.ts"],"mappings":";;;;;;;;YAUY;;WAED,mBAAmB;;WAEnB"}
@@ -0,0 +1,146 @@
1
+ import { keyName, parseSourceFile } from "../syntax/ast.mjs";
2
+ import { calleeName, literalString, unwrap, walk } from "./source-ast.mjs";
3
+ import path from "node:path";
4
+ import { readFileSync } from "node:fs";
5
+
6
+ //#region ../../packages/core/src/tooling/check/api-route-references.ts
7
+ const EMPTY_API_ROUTE_FACTS = {
8
+ clientHiddenNames: /* @__PURE__ */ new Set(),
9
+ declaresApiRoutes: false
10
+ };
11
+ /** Browser-side route-taking helpers from `@warlock.js/web`. */
12
+ const ROUTE_CALLS = /* @__PURE__ */ new Set(["apiHref", "useSubmitForm"]);
13
+ /** The value of a literal key on an object literal argument, when there is one. */
14
+ function objectProperty(object, name) {
15
+ const value = unwrap(object);
16
+ if (value.type !== "ObjectExpression") return void 0;
17
+ for (const property of value.properties) {
18
+ if (property.type !== "Property") continue;
19
+ if (keyName(property.key) === name) return property.value;
20
+ }
21
+ }
22
+ /**
23
+ * Names of the routes a source file registers with `clientVisible: false`
24
+ * inside an options object that also carries a literal `name`. A group prefix
25
+ * is not applied here, so a route whose name comes only from a group is not
26
+ * recognised; name such a route with a literal `name` option.
27
+ */
28
+ function clientHiddenRouteNamesOf(source) {
29
+ const names = [];
30
+ walk(source, (node) => {
31
+ if (node.type !== "CallExpression") return;
32
+ for (const argument of node.arguments) {
33
+ const hidden = objectProperty(argument, "clientVisible");
34
+ const flag = hidden === void 0 ? void 0 : unwrap(hidden);
35
+ const name = literalString(objectProperty(argument, "name"));
36
+ if (flag?.type === "Literal" && flag.value === false && name !== void 0) names.push(name);
37
+ }
38
+ }, true);
39
+ return names;
40
+ }
41
+ /** Whether a setup file's exported `config` object declares an `apiRoutes` key. */
42
+ function declaresApiRoutesOf(source) {
43
+ let declared = false;
44
+ walk(source, (node) => {
45
+ if (declared) return false;
46
+ if (node.type !== "VariableDeclarator" || keyName(node.id) !== "config") return;
47
+ if (node.init === null || node.init === void 0) return;
48
+ if (objectProperty(node.init, "apiRoutes") !== void 0) declared = true;
49
+ }, true);
50
+ return declared;
51
+ }
52
+ /**
53
+ * The static name a route-taking call receives: a string literal, a template
54
+ * with no substitutions, or the static prefix of a template. `undefined` means
55
+ * the name is only known at run time.
56
+ */
57
+ function staticNameOf(node) {
58
+ const value = unwrap(node);
59
+ const literal = literalString(value);
60
+ if (literal !== void 0) return {
61
+ name: literal,
62
+ prefix: false
63
+ };
64
+ if (value.type === "TemplateLiteral" && value.expressions.length > 0) {
65
+ const head = value.quasis[0]?.value.cooked ?? "";
66
+ return head.length > 0 ? {
67
+ name: head,
68
+ prefix: true
69
+ } : void 0;
70
+ }
71
+ }
72
+ /** The route-name argument of an `apiHref(name)` or `useSubmitForm({ route })` call. */
73
+ function routeArgumentOf(call) {
74
+ const first = call.arguments[0];
75
+ if (first === void 0) return void 0;
76
+ if (calleeName(call) === "apiHref") return first;
77
+ return objectProperty(first, "route");
78
+ }
79
+ /**
80
+ * The browser route-reference conventions:
81
+ *
82
+ * - `WL9020` (error) a web module passes a name of a `clientVisible: false`
83
+ * route to `apiHref()` or `useSubmitForm({ route })`, literally or as a
84
+ * template prefix;
85
+ * - `WL9019` (warning) such a call takes a name that cannot be read statically
86
+ * and no page declares `config.apiRoutes`, so the page's route table cannot
87
+ * be computed. The declaration is checked app-wide, not per page.
88
+ *
89
+ * Applies to `src/web/**` modules, outside test files.
90
+ */
91
+ function apiRouteReferenceFindings(source, facts = EMPTY_API_ROUTE_FACTS) {
92
+ if (!/^src\/web\/.*\.[cm]?[jt]sx?$/.test(source.fileName)) return [];
93
+ if (/\.(?:test|spec)\.[cm]?[jt]sx?$/i.test(source.fileName)) return [];
94
+ const findings = [];
95
+ walk(source, (node) => {
96
+ if (node.type !== "CallExpression") return;
97
+ if (!ROUTE_CALLS.has(calleeName(node) ?? "")) return;
98
+ const argument = routeArgumentOf(node);
99
+ if (argument === void 0) return;
100
+ const target = staticNameOf(argument);
101
+ if (target === void 0) {
102
+ if (!facts.declaresApiRoutes) findings.push({
103
+ code: "WL9019",
104
+ node: argument,
105
+ severity: "warning",
106
+ message: "This route name is only known at run time, so the page's browser route table cannot include it. Pass a string literal, or declare the names with config.apiRoutes in the page's setup file."
107
+ });
108
+ return;
109
+ }
110
+ const hidden = [...facts.clientHiddenNames].find((name) => target.prefix ? name.startsWith(target.name) : name === target.name);
111
+ if (hidden !== void 0) findings.push({
112
+ code: "WL9020",
113
+ node: argument,
114
+ severity: "error",
115
+ message: `Route "${hidden}" is registered with clientVisible: false and cannot be referenced from browser code.`
116
+ });
117
+ }, true);
118
+ return findings;
119
+ }
120
+ /**
121
+ * Gathers the project-wide facts from `files` (project-relative). Unreadable or
122
+ * unparsable files contribute nothing, as the per-file checks report them.
123
+ */
124
+ function collectApiRouteFacts(cwd, files) {
125
+ const clientHiddenNames = /* @__PURE__ */ new Set();
126
+ let declaresApiRoutes = false;
127
+ for (const file of files) {
128
+ if (!/^src\/.*\.[cm]?[jt]sx?$/.test(file)) continue;
129
+ let sourceFile;
130
+ try {
131
+ sourceFile = parseSourceFile(file, readFileSync(path.join(cwd, file), "utf8"));
132
+ } catch {
133
+ continue;
134
+ }
135
+ for (const name of clientHiddenRouteNamesOf(sourceFile)) clientHiddenNames.add(name);
136
+ if (/^src\/web\/.*\.setup\.[cm]?[jt]sx?$/.test(file) && declaresApiRoutesOf(sourceFile)) declaresApiRoutes = true;
137
+ }
138
+ return {
139
+ clientHiddenNames,
140
+ declaresApiRoutes
141
+ };
142
+ }
143
+
144
+ //#endregion
145
+ export { EMPTY_API_ROUTE_FACTS, apiRouteReferenceFindings, clientHiddenRouteNamesOf, collectApiRouteFacts, declaresApiRoutesOf };
146
+ //# sourceMappingURL=api-route-references.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-route-references.mjs","names":["ast.keyName","ast.parseSourceFile"],"sources":["../../../../../../../../../packages/core/src/tooling/check/api-route-references.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport * as ast from \"../syntax/ast\";\nimport { calleeName, literalString, unwrap, walk, type SourceFinding } from \"./source-ast\";\n\n/**\n * Project-wide facts the browser-reference checks need. Gathered once per run\n * (`runConventions`) because a route is registered in one file and referenced\n * from another.\n */\nexport type ApiRouteFacts = {\n /** Names of routes registered with `clientVisible: false`. */\n readonly clientHiddenNames: ReadonlySet<string>;\n /** Whether any page, layout or root setup file declares `config.apiRoutes`. */\n readonly declaresApiRoutes: boolean;\n};\n\nexport const EMPTY_API_ROUTE_FACTS: ApiRouteFacts = {\n clientHiddenNames: new Set(),\n declaresApiRoutes: false,\n};\n\n/** Browser-side route-taking helpers from `@warlock.js/web`. */\nconst ROUTE_CALLS = new Set([\"apiHref\", \"useSubmitForm\"]);\n\n/** The value of a literal key on an object literal argument, when there is one. */\nfunction objectProperty(object: ast.Node, name: string): ast.Node | undefined {\n const value = unwrap(object);\n if (value.type !== \"ObjectExpression\") return undefined;\n for (const property of value.properties) {\n if (property.type !== \"Property\") continue;\n if (ast.keyName(property.key) === name) return property.value;\n }\n return undefined;\n}\n\n/**\n * Names of the routes a source file registers with `clientVisible: false`\n * inside an options object that also carries a literal `name`. A group prefix\n * is not applied here, so a route whose name comes only from a group is not\n * recognised; name such a route with a literal `name` option.\n */\nexport function clientHiddenRouteNamesOf(source: ast.SourceFile): string[] {\n const names: string[] = [];\n\n walk(\n source,\n (node) => {\n if (node.type !== \"CallExpression\") return;\n for (const argument of node.arguments) {\n const hidden = objectProperty(argument, \"clientVisible\");\n const flag = hidden === undefined ? undefined : unwrap(hidden);\n const name = literalString(objectProperty(argument, \"name\"));\n if (flag?.type === \"Literal\" && flag.value === false && name !== undefined)\n names.push(name);\n }\n },\n true,\n );\n\n return names;\n}\n\n/** Whether a setup file's exported `config` object declares an `apiRoutes` key. */\nexport function declaresApiRoutesOf(source: ast.SourceFile): boolean {\n let declared = false;\n\n walk(\n source,\n (node) => {\n if (declared) return false;\n if (node.type !== \"VariableDeclarator\" || ast.keyName(node.id) !== \"config\") return;\n if (node.init === null || node.init === undefined) return;\n if (objectProperty(node.init, \"apiRoutes\") !== undefined) declared = true;\n },\n true,\n );\n\n return declared;\n}\n\n/**\n * The static name a route-taking call receives: a string literal, a template\n * with no substitutions, or the static prefix of a template. `undefined` means\n * the name is only known at run time.\n */\nfunction staticNameOf(node: ast.Node): { name: string; prefix: boolean } | undefined {\n const value = unwrap(node);\n const literal = literalString(value);\n if (literal !== undefined) return { name: literal, prefix: false };\n if (value.type === \"TemplateLiteral\" && value.expressions.length > 0) {\n const head = value.quasis[0]?.value.cooked ?? \"\";\n return head.length > 0 ? { name: head, prefix: true } : undefined;\n }\n return undefined;\n}\n\n/** The route-name argument of an `apiHref(name)` or `useSubmitForm({ route })` call. */\nfunction routeArgumentOf(call: ast.CallExpression): ast.Node | undefined {\n const first = call.arguments[0];\n if (first === undefined) return undefined;\n if (calleeName(call) === \"apiHref\") return first;\n return objectProperty(first, \"route\");\n}\n\n/**\n * The browser route-reference conventions:\n *\n * - `WL9020` (error) a web module passes a name of a `clientVisible: false`\n * route to `apiHref()` or `useSubmitForm({ route })`, literally or as a\n * template prefix;\n * - `WL9019` (warning) such a call takes a name that cannot be read statically\n * and no page declares `config.apiRoutes`, so the page's route table cannot\n * be computed. The declaration is checked app-wide, not per page.\n *\n * Applies to `src/web/**` modules, outside test files.\n */\nexport function apiRouteReferenceFindings(\n source: ast.SourceFile,\n facts: ApiRouteFacts = EMPTY_API_ROUTE_FACTS,\n): SourceFinding[] {\n if (!/^src\\/web\\/.*\\.[cm]?[jt]sx?$/.test(source.fileName)) return [];\n if (/\\.(?:test|spec)\\.[cm]?[jt]sx?$/i.test(source.fileName)) return [];\n\n const findings: SourceFinding[] = [];\n\n walk(\n source,\n (node) => {\n if (node.type !== \"CallExpression\") return;\n if (!ROUTE_CALLS.has(calleeName(node) ?? \"\")) return;\n const argument = routeArgumentOf(node);\n if (argument === undefined) return;\n const target = staticNameOf(argument);\n\n if (target === undefined) {\n if (!facts.declaresApiRoutes)\n findings.push({\n code: \"WL9019\",\n node: argument,\n severity: \"warning\",\n message:\n \"This route name is only known at run time, so the page's browser route table cannot include it. Pass a string literal, or declare the names with config.apiRoutes in the page's setup file.\",\n });\n return;\n }\n\n const hidden = [...facts.clientHiddenNames].find((name) =>\n target.prefix ? name.startsWith(target.name) : name === target.name,\n );\n\n if (hidden !== undefined)\n findings.push({\n code: \"WL9020\",\n node: argument,\n severity: \"error\",\n message: `Route \"${hidden}\" is registered with clientVisible: false and cannot be referenced from browser code.`,\n });\n },\n true,\n );\n\n return findings;\n}\n\n/**\n * Gathers the project-wide facts from `files` (project-relative). Unreadable or\n * unparsable files contribute nothing, as the per-file checks report them.\n */\nexport function collectApiRouteFacts(cwd: string, files: readonly string[]): ApiRouteFacts {\n const clientHiddenNames = new Set<string>();\n let declaresApiRoutes = false;\n\n for (const file of files) {\n if (!/^src\\/.*\\.[cm]?[jt]sx?$/.test(file)) continue;\n\n let sourceFile: ast.SourceFile;\n\n try {\n sourceFile = ast.parseSourceFile(file, readFileSync(path.join(cwd, file), \"utf8\"));\n } catch {\n continue;\n }\n\n for (const name of clientHiddenRouteNamesOf(sourceFile)) clientHiddenNames.add(name);\n if (/^src\\/web\\/.*\\.setup\\.[cm]?[jt]sx?$/.test(file) && declaresApiRoutesOf(sourceFile))\n declaresApiRoutes = true;\n }\n\n return { clientHiddenNames, declaresApiRoutes };\n}\n"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { ToolingProblem } from "../problem.mjs";
2
2
  import { ConventionOptions } from "./web-conventions.mjs";
3
+ import { ApiRouteFacts } from "./api-route-references.mjs";
3
4
  //#region ../../packages/core/src/tooling/check/conventions-step.d.ts
4
5
  /** Matches a project-relative path of a file the framework loads by itself. */
5
6
  export declare function isAutoLoadedFile(relativePath: string): boolean;
@@ -16,18 +17,20 @@ export declare function isAutoLoadedFile(relativePath: string): boolean;
16
17
  * and no `public: true`, a redirect target from request input, sensitive
17
18
  * values in logs, unbounded regexes on request input;
18
19
  * - `WL9010`–`WL9012` warnings: `await log.*()`, a second controller in one
19
- * file, a `kind` discriminator.
20
+ * file, a `kind` discriminator;
21
+ * - `WL9019` (warning) and `WL9020` (error) the browser route references
22
+ * (see `apiRouteReferenceFindings`).
20
23
  *
21
24
  * A `// warlock-check-ignore WL9006` comment on the problem's line or the line
22
25
  * above silences that code there; without codes it silences every one.
23
26
  */
24
- export declare function checkFileConventions(relativePath: string, source: string, cwd: string, options?: ConventionOptions): ToolingProblem[];
27
+ export declare function checkFileConventions(relativePath: string, source: string, cwd: string, options?: ConventionOptions, apiRouteFacts?: ApiRouteFacts): ToolingProblem[];
25
28
  /** `WL9004` when the project tsconfig turns on legacy decorators. */
26
29
  export declare function checkTsconfigConventions(cwd: string, project?: string): ToolingProblem[];
27
30
  /**
28
31
  * Run every convention check over `files` (project-relative) plus the
29
32
  * project tsconfig.
30
33
  */
31
- export declare function runConventions(cwd: string, files: readonly string[], project?: string): ToolingProblem[];
34
+ export declare function runConventions(cwd: string, files: readonly string[], project?: string, apiRouteFacts?: ApiRouteFacts): ToolingProblem[];
32
35
  //#endregion
33
36
  //# sourceMappingURL=conventions-step.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"conventions-step.d.mts","names":[],"sources":["../../../../../../../../../packages/core/src/tooling/check/conventions-step.ts"],"mappings":";;;;wBAoBgB,iBAAiB;;;;;;;;;;;;;;;;;;;wBAuKjB,qBACd,sBACA,gBACA,aACA,UAAS,oBACR;;wBAgCa,yBAAyB,aAAa,mBAA4B;;;;;wBAyClE,eACd,aACA,0BACA,mBACC"}
1
+ {"version":3,"file":"conventions-step.d.mts","names":[],"sources":["../../../../../../../../../packages/core/src/tooling/check/conventions-step.ts"],"mappings":";;;;;wBA2BgB,iBAAiB;;;;;;;;;;;;;;;;;;;;;wBAyKjB,qBACd,sBACA,gBACA,aACA,UAAS,mBACT,gBAAe,gBACd;;wBAkCa,yBAAyB,aAAa,mBAA4B;;;;;wBAyClE,eACd,aACA,0BACA,kBACA,gBAAe,gBACd"}
@@ -7,6 +7,8 @@ import { positionOf } from "./source-ast.mjs";
7
7
  import { codeStyleFindings } from "./code-style-conventions.mjs";
8
8
  import { securityFindings } from "./security-conventions.mjs";
9
9
  import { responseSchemaFindings, webFindings } from "./web-conventions.mjs";
10
+ import { EMPTY_API_ROUTE_FACTS, apiRouteReferenceFindings, collectApiRouteFacts } from "./api-route-references.mjs";
11
+ import { translationProblems } from "./translation-conventions.mjs";
10
12
  import path from "node:path";
11
13
  import { existsSync, readFileSync } from "node:fs";
12
14
  import { parse } from "jsonc-parser";
@@ -123,19 +125,26 @@ function findingProblem(sourceFile, relativePath, finding) {
123
125
  * and no `public: true`, a redirect target from request input, sensitive
124
126
  * values in logs, unbounded regexes on request input;
125
127
  * - `WL9010`–`WL9012` warnings: `await log.*()`, a second controller in one
126
- * file, a `kind` discriminator.
128
+ * file, a `kind` discriminator;
129
+ * - `WL9019` (warning) and `WL9020` (error) the browser route references
130
+ * (see `apiRouteReferenceFindings`).
127
131
  *
128
132
  * A `// warlock-check-ignore WL9006` comment on the problem's line or the line
129
133
  * above silences that code there; without codes it silences every one.
130
134
  */
131
- function checkFileConventions(relativePath, source, cwd, options = {}) {
135
+ function checkFileConventions(relativePath, source, cwd, options = {}, apiRouteFacts = EMPTY_API_ROUTE_FACTS) {
132
136
  const sourceFile = parseSourceFile(relativePath, source);
133
- const problems = [...importProblems(sourceFile, relativePath, cwd), ...[
134
- ...securityFindings(sourceFile),
135
- ...codeStyleFindings(sourceFile),
136
- ...webFindings(sourceFile, options),
137
- ...responseSchemaFindings(sourceFile)
138
- ].map((finding) => findingProblem(sourceFile, relativePath, finding))];
137
+ const problems = [
138
+ ...importProblems(sourceFile, relativePath, cwd),
139
+ ...translationProblems(relativePath, source),
140
+ ...[
141
+ ...securityFindings(sourceFile),
142
+ ...codeStyleFindings(sourceFile),
143
+ ...webFindings(sourceFile, options),
144
+ ...responseSchemaFindings(sourceFile),
145
+ ...apiRouteReferenceFindings(sourceFile, apiRouteFacts)
146
+ ].map((finding) => findingProblem(sourceFile, relativePath, finding))
147
+ ];
139
148
  const lines = source.split(/\r?\n/);
140
149
  return problems.filter((problem) => options.rules?.[problem.code] !== "off").map((problem) => ({
141
150
  ...problem,
@@ -167,7 +176,7 @@ function checkTsconfigConventions(cwd, project = "tsconfig.json") {
167
176
  * Run every convention check over `files` (project-relative) plus the
168
177
  * project tsconfig.
169
178
  */
170
- function runConventions(cwd, files, project) {
179
+ function runConventions(cwd, files, project, apiRouteFacts = collectApiRouteFacts(cwd, files)) {
171
180
  const options = readConventionOptions(cwd);
172
181
  const problems = checkTsconfigConventions(cwd, project).filter((problem) => options.rules?.[problem.code] !== "off").map((problem) => {
173
182
  const severity = options.rules?.[problem.code];
@@ -183,7 +192,7 @@ function runConventions(cwd, files, project) {
183
192
  } catch {
184
193
  continue;
185
194
  }
186
- problems.push(...checkFileConventions(file, source, cwd, options));
195
+ problems.push(...checkFileConventions(file, source, cwd, options, apiRouteFacts));
187
196
  }
188
197
  return problems;
189
198
  }
@@ -1 +1 @@
1
- {"version":3,"file":"conventions-step.mjs","names":["ast.parseSourceFile"],"sources":["../../../../../../../../../packages/core/src/tooling/check/conventions-step.ts"],"sourcesContent":["import { isWarlockErrorCode } from \"../../errors/error-codes\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { parse } from \"jsonc-parser\";\nimport { importSpecifiers } from \"../syntax/imports\";\nimport * as ast from \"../syntax/ast\";\nimport { createProblem, type ToolingProblem } from \"../problem\";\nimport { importFixes } from \"./fixes\";\nimport { codeStyleFindings } from \"./code-style-conventions\";\nimport { securityFindings } from \"./security-conventions\";\nimport { webFindings, responseSchemaFindings, type ConventionOptions } from \"./web-conventions\";\nimport { positionOf, type SourceFinding } from \"./source-ast\";\n\n/** Extensions tried when resolving an extension-less import. */\nconst RESOLVE_EXTENSIONS = [\".ts\", \".tsx\", \".js\", \".jsx\", \"/index.ts\", \"/index.tsx\"];\n\n/** Test files may import anything on purpose. */\nconst TEST_FILE = /\\.(?:test|spec)\\.[cm]?[jt]sx?$/i;\n\n/** Matches a project-relative path of a file the framework loads by itself. */\nexport function isAutoLoadedFile(relativePath: string): boolean {\n return (\n /^src\\/app\\/main\\.tsx?$/.test(relativePath) ||\n /^src\\/app\\/[^/]+\\/(?:routes|main|worker)\\.tsx?$/.test(relativePath) ||\n /^src\\/app\\/.+\\/events\\/[^/]+\\.tsx?$/.test(relativePath)\n );\n}\n\n/** The module folder (`src/app/<module>`) a project-relative path lives in, if any. */\nfunction moduleOf(relativePath: string): string | undefined {\n return /^src\\/app\\/([^/]+)\\//.exec(relativePath)?.[1];\n}\n\nfunction toPosix(file: string): string {\n return file.split(path.sep).join(\"/\");\n}\n\n/** Resolve a relative or `app/...` specifier to a project-relative file, if it exists. */\nexport function resolveSpecifier(\n specifier: string,\n importer: string,\n cwd: string,\n): string | undefined {\n let base: string;\n\n if (specifier.startsWith(\".\")) {\n base = path.resolve(path.dirname(path.join(cwd, importer)), specifier);\n } else if (specifier.startsWith(\"app/\")) {\n base = path.join(cwd, \"src\", specifier);\n } else {\n return undefined;\n }\n\n const candidates = [base, ...RESOLVE_EXTENSIONS.map((extension) => `${base}${extension}`)];\n const withoutJs = base.replace(/\\.jsx?$/, \"\");\n\n if (withoutJs !== base) {\n candidates.push(...RESOLVE_EXTENSIONS.map((extension) => `${withoutJs}${extension}`));\n }\n\n const found = candidates.find(\n (candidate) => existsSync(candidate) && path.extname(candidate) !== \"\",\n );\n\n return found ? toPosix(path.relative(cwd, found)) : undefined;\n}\n\n/** Every module specifier a source file imports, with its position. */\nfunction importsOf(sourceFile: ast.SourceFile): { specifier: string; node: ast.Node }[] {\n return importSpecifiers(sourceFile.fileName, sourceFile.text);\n}\n\n/** `// warlock-check-ignore [WLxxxx …]` on a problem's line or the line above it. */\nconst IGNORE_COMMENT = /\\/[/*]\\s*warlock-check-ignore\\b(.*)/;\n\n/** Whether an ignore comment on `line` (1-based) or the line before covers `code`. */\nfunction isIgnored(lines: readonly string[], line: number, code: string): boolean {\n for (const text of [lines[line - 1], lines[line - 2]]) {\n const match = text === undefined ? null : IGNORE_COMMENT.exec(text);\n\n if (!match) continue;\n\n const codes = match[1]?.match(/WL\\d{4}/g);\n\n if (!codes || codes.includes(code)) return true;\n }\n\n return false;\n}\n\n/** The import conventions (`WL9001`–`WL9003`) of one parsed file. */\nfunction importProblems(\n sourceFile: ast.SourceFile,\n relativePath: string,\n cwd: string,\n): ToolingProblem[] {\n const problems: ToolingProblem[] = [];\n const fixes = importFixes(relativePath, sourceFile.text);\n const importerModule = moduleOf(relativePath);\n\n for (const { specifier, node } of importsOf(sourceFile)) {\n const at = { file: relativePath, ...positionOf(sourceFile, node) };\n\n if (specifier.startsWith(\".\") && /\\.jsx?$/.test(specifier)) {\n problems.push(\n createProblem({\n code: \"WL9002\",\n fix: fixes.find((fix) => fix.end === node.end - 1),\n source: \"convention\",\n message: `Import \"${specifier}\" ends in a .js extension.`,\n ...at,\n }),\n );\n }\n\n const target = resolveSpecifier(specifier, relativePath, cwd);\n\n if (!target) continue;\n\n if (!TEST_FILE.test(relativePath) && isAutoLoadedFile(target)) {\n problems.push(\n createProblem({\n code: \"WL9001\",\n source: \"convention\",\n message: `\"${specifier}\" imports ${target}, which the framework loads by itself.`,\n ...at,\n }),\n );\n }\n\n const targetModule = moduleOf(target);\n\n if (\n specifier.startsWith(\".\") &&\n importerModule !== undefined &&\n targetModule !== undefined &&\n importerModule !== targetModule\n ) {\n const alias = `app/${target.replace(/^src\\/app\\//, \"\").replace(/\\.[cm]?[jt]sx?$/, \"\")}`;\n\n problems.push(\n createProblem({\n code: \"WL9003\",\n source: \"convention\",\n message: `\"${specifier}\" reaches from module \"${importerModule}\" into \"${targetModule}\"; import \"${alias}\" instead.`,\n ...at,\n }),\n );\n }\n }\n\n return problems;\n}\n\nfunction findingProblem(\n sourceFile: ast.SourceFile,\n relativePath: string,\n finding: SourceFinding,\n): ToolingProblem {\n return createProblem({\n code: finding.code,\n source: \"convention\",\n severity: finding.severity ?? \"error\",\n message: finding.message,\n file: relativePath,\n ...positionOf(sourceFile, finding.node),\n });\n}\n\n/**\n * The convention problems in one source file, in source order:\n *\n * - `WL9001` an import of a file the framework loads by itself (`routes.ts`,\n * `main.ts`, `worker.ts`, `events/*`) — importing it registers it twice;\n * - `WL9002` a relative import ending in `.js`;\n * - `WL9003` a relative import from one `src/app/<module>` into another, which\n * should use the `app/<module>/...` alias;\n * - `WL9005`–`WL9009` the security conventions (see `securityFindings`):\n * `request.all()` into a query or service, a mutating route with no guard\n * and no `public: true`, a redirect target from request input, sensitive\n * values in logs, unbounded regexes on request input;\n * - `WL9010`–`WL9012` warnings: `await log.*()`, a second controller in one\n * file, a `kind` discriminator.\n *\n * A `// warlock-check-ignore WL9006` comment on the problem's line or the line\n * above silences that code there; without codes it silences every one.\n */\nexport function checkFileConventions(\n relativePath: string,\n source: string,\n cwd: string,\n options: ConventionOptions = {},\n): ToolingProblem[] {\n const sourceFile = ast.parseSourceFile(relativePath, source);\n const problems = [\n ...importProblems(sourceFile, relativePath, cwd),\n ...[\n ...securityFindings(sourceFile),\n ...codeStyleFindings(sourceFile),\n ...webFindings(sourceFile, options),\n ...responseSchemaFindings(sourceFile),\n ].map((finding) => findingProblem(sourceFile, relativePath, finding)),\n ];\n const lines = source.split(/\\r?\\n/);\n\n return problems\n .filter((problem) => options.rules?.[problem.code] !== \"off\")\n .map((problem) => ({\n ...problem,\n severity:\n options.rules?.[problem.code] === \"error\"\n ? (\"error\" as const)\n : options.rules?.[problem.code] === \"warning\"\n ? (\"warning\" as const)\n : problem.severity,\n }))\n .filter((problem) => problem.line === null || !isIgnored(lines, problem.line, problem.code))\n .sort(\n (first, second) =>\n (first.line ?? 0) - (second.line ?? 0) || (first.column ?? 0) - (second.column ?? 0),\n );\n}\n\n/** `WL9004` when the project tsconfig turns on legacy decorators. */\nexport function checkTsconfigConventions(cwd: string, project = \"tsconfig.json\"): ToolingProblem[] {\n const file = path.join(cwd, project);\n\n if (!existsSync(file)) return [];\n\n const text = readFileSync(file, \"utf8\");\n const config: unknown = parse(text);\n const options: unknown =\n typeof config === \"object\" && config !== null\n ? Reflect.get(config, \"compilerOptions\")\n : undefined;\n\n if (\n typeof options !== \"object\" ||\n options === null ||\n Reflect.get(options, \"experimentalDecorators\") !== true\n ) {\n return [];\n }\n\n const index = text.indexOf(\"experimentalDecorators\");\n const before = index === -1 ? \"\" : text.slice(0, index);\n const line = index === -1 ? null : before.split(\"\\n\").length;\n const column = index === -1 ? null : index - before.lastIndexOf(\"\\n\");\n\n return [\n createProblem({\n code: \"WL9004\",\n source: \"convention\",\n message: '\"experimentalDecorators\": true switches TypeScript to legacy decorators.',\n file: toPosix(project),\n line,\n column,\n }),\n ];\n}\n\n/**\n * Run every convention check over `files` (project-relative) plus the\n * project tsconfig.\n */\nexport function runConventions(\n cwd: string,\n files: readonly string[],\n project?: string,\n): ToolingProblem[] {\n const options = readConventionOptions(cwd);\n const problems = checkTsconfigConventions(cwd, project)\n .filter((problem) => options.rules?.[problem.code] !== \"off\")\n .map((problem) => {\n const severity = options.rules?.[problem.code];\n return severity === \"error\" || severity === \"warning\" ? { ...problem, severity } : problem;\n });\n\n for (const file of files) {\n let source: string;\n\n try {\n source = readFileSync(path.join(cwd, file), \"utf8\");\n } catch {\n continue;\n }\n\n problems.push(...checkFileConventions(file, source, cwd, options));\n }\n\n return problems;\n}\n\n/** Optional JSON configuration; malformed configuration is reported by the check step. */\nexport function readConventionOptions(cwd: string): ConventionOptions {\n const file = path.join(cwd, \".warlock-check.json\");\n if (!existsSync(file)) return {};\n const value: unknown = JSON.parse(readFileSync(file, \"utf8\"));\n if (!value || typeof value !== \"object\")\n throw new Error(\".warlock-check.json must be an object.\");\n const limit: unknown = Reflect.get(value, \"componentLineLimit\");\n if (limit !== undefined && (typeof limit !== \"number\" || !Number.isInteger(limit) || limit < 1))\n throw new Error(\"componentLineLimit must be a positive integer.\");\n const rules: unknown = Reflect.get(value, \"rules\");\n const overrides: NonNullable<ConventionOptions[\"rules\"]> = {};\n if (rules !== undefined) {\n if (!rules || typeof rules !== \"object\" || Array.isArray(rules))\n throw new Error(\"rules must be an object.\");\n for (const [code, severity] of Object.entries(rules)) {\n if (\n !isWarlockErrorCode(code) ||\n (severity !== \"off\" && severity !== \"warning\" && severity !== \"error\")\n )\n throw new Error(\"rules must map registered WL codes to off, warning or error.\");\n overrides[code] = severity;\n }\n }\n return {\n ...(typeof limit === \"number\" ? { componentLineLimit: limit } : {}),\n rules: overrides,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"conventions-step.mjs","names":["ast.parseSourceFile"],"sources":["../../../../../../../../../packages/core/src/tooling/check/conventions-step.ts"],"sourcesContent":["import { isWarlockErrorCode } from \"../../errors/error-codes\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { parse } from \"jsonc-parser\";\nimport { importSpecifiers } from \"../syntax/imports\";\nimport * as ast from \"../syntax/ast\";\nimport { createProblem, type ToolingProblem } from \"../problem\";\nimport { importFixes } from \"./fixes\";\nimport { codeStyleFindings } from \"./code-style-conventions\";\nimport { securityFindings } from \"./security-conventions\";\nimport { webFindings, responseSchemaFindings, type ConventionOptions } from \"./web-conventions\";\nimport {\n apiRouteReferenceFindings,\n collectApiRouteFacts,\n EMPTY_API_ROUTE_FACTS,\n type ApiRouteFacts,\n} from \"./api-route-references\";\nimport { translationProblems } from \"./translation-conventions\";\nimport { positionOf, type SourceFinding } from \"./source-ast\";\n\n/** Extensions tried when resolving an extension-less import. */\nconst RESOLVE_EXTENSIONS = [\".ts\", \".tsx\", \".js\", \".jsx\", \"/index.ts\", \"/index.tsx\"];\n\n/** Test files may import anything on purpose. */\nconst TEST_FILE = /\\.(?:test|spec)\\.[cm]?[jt]sx?$/i;\n\n/** Matches a project-relative path of a file the framework loads by itself. */\nexport function isAutoLoadedFile(relativePath: string): boolean {\n return (\n /^src\\/app\\/main\\.tsx?$/.test(relativePath) ||\n /^src\\/app\\/[^/]+\\/(?:routes|main|worker)\\.tsx?$/.test(relativePath) ||\n /^src\\/app\\/.+\\/events\\/[^/]+\\.tsx?$/.test(relativePath)\n );\n}\n\n/** The module folder (`src/app/<module>`) a project-relative path lives in, if any. */\nfunction moduleOf(relativePath: string): string | undefined {\n return /^src\\/app\\/([^/]+)\\//.exec(relativePath)?.[1];\n}\n\nfunction toPosix(file: string): string {\n return file.split(path.sep).join(\"/\");\n}\n\n/** Resolve a relative or `app/...` specifier to a project-relative file, if it exists. */\nexport function resolveSpecifier(\n specifier: string,\n importer: string,\n cwd: string,\n): string | undefined {\n let base: string;\n\n if (specifier.startsWith(\".\")) {\n base = path.resolve(path.dirname(path.join(cwd, importer)), specifier);\n } else if (specifier.startsWith(\"app/\")) {\n base = path.join(cwd, \"src\", specifier);\n } else {\n return undefined;\n }\n\n const candidates = [base, ...RESOLVE_EXTENSIONS.map((extension) => `${base}${extension}`)];\n const withoutJs = base.replace(/\\.jsx?$/, \"\");\n\n if (withoutJs !== base) {\n candidates.push(...RESOLVE_EXTENSIONS.map((extension) => `${withoutJs}${extension}`));\n }\n\n const found = candidates.find(\n (candidate) => existsSync(candidate) && path.extname(candidate) !== \"\",\n );\n\n return found ? toPosix(path.relative(cwd, found)) : undefined;\n}\n\n/** Every module specifier a source file imports, with its position. */\nfunction importsOf(sourceFile: ast.SourceFile): { specifier: string; node: ast.Node }[] {\n return importSpecifiers(sourceFile.fileName, sourceFile.text);\n}\n\n/** `// warlock-check-ignore [WLxxxx …]` on a problem's line or the line above it. */\nconst IGNORE_COMMENT = /\\/[/*]\\s*warlock-check-ignore\\b(.*)/;\n\n/** Whether an ignore comment on `line` (1-based) or the line before covers `code`. */\nfunction isIgnored(lines: readonly string[], line: number, code: string): boolean {\n for (const text of [lines[line - 1], lines[line - 2]]) {\n const match = text === undefined ? null : IGNORE_COMMENT.exec(text);\n\n if (!match) continue;\n\n const codes = match[1]?.match(/WL\\d{4}/g);\n\n if (!codes || codes.includes(code)) return true;\n }\n\n return false;\n}\n\n/** The import conventions (`WL9001`–`WL9003`) of one parsed file. */\nfunction importProblems(\n sourceFile: ast.SourceFile,\n relativePath: string,\n cwd: string,\n): ToolingProblem[] {\n const problems: ToolingProblem[] = [];\n const fixes = importFixes(relativePath, sourceFile.text);\n const importerModule = moduleOf(relativePath);\n\n for (const { specifier, node } of importsOf(sourceFile)) {\n const at = { file: relativePath, ...positionOf(sourceFile, node) };\n\n if (specifier.startsWith(\".\") && /\\.jsx?$/.test(specifier)) {\n problems.push(\n createProblem({\n code: \"WL9002\",\n fix: fixes.find((fix) => fix.end === node.end - 1),\n source: \"convention\",\n message: `Import \"${specifier}\" ends in a .js extension.`,\n ...at,\n }),\n );\n }\n\n const target = resolveSpecifier(specifier, relativePath, cwd);\n\n if (!target) continue;\n\n if (!TEST_FILE.test(relativePath) && isAutoLoadedFile(target)) {\n problems.push(\n createProblem({\n code: \"WL9001\",\n source: \"convention\",\n message: `\"${specifier}\" imports ${target}, which the framework loads by itself.`,\n ...at,\n }),\n );\n }\n\n const targetModule = moduleOf(target);\n\n if (\n specifier.startsWith(\".\") &&\n importerModule !== undefined &&\n targetModule !== undefined &&\n importerModule !== targetModule\n ) {\n const alias = `app/${target.replace(/^src\\/app\\//, \"\").replace(/\\.[cm]?[jt]sx?$/, \"\")}`;\n\n problems.push(\n createProblem({\n code: \"WL9003\",\n source: \"convention\",\n message: `\"${specifier}\" reaches from module \"${importerModule}\" into \"${targetModule}\"; import \"${alias}\" instead.`,\n ...at,\n }),\n );\n }\n }\n\n return problems;\n}\n\nfunction findingProblem(\n sourceFile: ast.SourceFile,\n relativePath: string,\n finding: SourceFinding,\n): ToolingProblem {\n return createProblem({\n code: finding.code,\n source: \"convention\",\n severity: finding.severity ?? \"error\",\n message: finding.message,\n file: relativePath,\n ...positionOf(sourceFile, finding.node),\n });\n}\n\n/**\n * The convention problems in one source file, in source order:\n *\n * - `WL9001` an import of a file the framework loads by itself (`routes.ts`,\n * `main.ts`, `worker.ts`, `events/*`) — importing it registers it twice;\n * - `WL9002` a relative import ending in `.js`;\n * - `WL9003` a relative import from one `src/app/<module>` into another, which\n * should use the `app/<module>/...` alias;\n * - `WL9005`–`WL9009` the security conventions (see `securityFindings`):\n * `request.all()` into a query or service, a mutating route with no guard\n * and no `public: true`, a redirect target from request input, sensitive\n * values in logs, unbounded regexes on request input;\n * - `WL9010`–`WL9012` warnings: `await log.*()`, a second controller in one\n * file, a `kind` discriminator;\n * - `WL9019` (warning) and `WL9020` (error) the browser route references\n * (see `apiRouteReferenceFindings`).\n *\n * A `// warlock-check-ignore WL9006` comment on the problem's line or the line\n * above silences that code there; without codes it silences every one.\n */\nexport function checkFileConventions(\n relativePath: string,\n source: string,\n cwd: string,\n options: ConventionOptions = {},\n apiRouteFacts: ApiRouteFacts = EMPTY_API_ROUTE_FACTS,\n): ToolingProblem[] {\n const sourceFile = ast.parseSourceFile(relativePath, source);\n const problems = [\n ...importProblems(sourceFile, relativePath, cwd),\n ...translationProblems(relativePath, source),\n ...[\n ...securityFindings(sourceFile),\n ...codeStyleFindings(sourceFile),\n ...webFindings(sourceFile, options),\n ...responseSchemaFindings(sourceFile),\n ...apiRouteReferenceFindings(sourceFile, apiRouteFacts),\n ].map((finding) => findingProblem(sourceFile, relativePath, finding)),\n ];\n const lines = source.split(/\\r?\\n/);\n\n return problems\n .filter((problem) => options.rules?.[problem.code] !== \"off\")\n .map((problem) => ({\n ...problem,\n severity:\n options.rules?.[problem.code] === \"error\"\n ? (\"error\" as const)\n : options.rules?.[problem.code] === \"warning\"\n ? (\"warning\" as const)\n : problem.severity,\n }))\n .filter((problem) => problem.line === null || !isIgnored(lines, problem.line, problem.code))\n .sort(\n (first, second) =>\n (first.line ?? 0) - (second.line ?? 0) || (first.column ?? 0) - (second.column ?? 0),\n );\n}\n\n/** `WL9004` when the project tsconfig turns on legacy decorators. */\nexport function checkTsconfigConventions(cwd: string, project = \"tsconfig.json\"): ToolingProblem[] {\n const file = path.join(cwd, project);\n\n if (!existsSync(file)) return [];\n\n const text = readFileSync(file, \"utf8\");\n const config: unknown = parse(text);\n const options: unknown =\n typeof config === \"object\" && config !== null\n ? Reflect.get(config, \"compilerOptions\")\n : undefined;\n\n if (\n typeof options !== \"object\" ||\n options === null ||\n Reflect.get(options, \"experimentalDecorators\") !== true\n ) {\n return [];\n }\n\n const index = text.indexOf(\"experimentalDecorators\");\n const before = index === -1 ? \"\" : text.slice(0, index);\n const line = index === -1 ? null : before.split(\"\\n\").length;\n const column = index === -1 ? null : index - before.lastIndexOf(\"\\n\");\n\n return [\n createProblem({\n code: \"WL9004\",\n source: \"convention\",\n message: '\"experimentalDecorators\": true switches TypeScript to legacy decorators.',\n file: toPosix(project),\n line,\n column,\n }),\n ];\n}\n\n/**\n * Run every convention check over `files` (project-relative) plus the\n * project tsconfig.\n */\nexport function runConventions(\n cwd: string,\n files: readonly string[],\n project?: string,\n apiRouteFacts: ApiRouteFacts = collectApiRouteFacts(cwd, files),\n): ToolingProblem[] {\n const options = readConventionOptions(cwd);\n const problems = checkTsconfigConventions(cwd, project)\n .filter((problem) => options.rules?.[problem.code] !== \"off\")\n .map((problem) => {\n const severity = options.rules?.[problem.code];\n return severity === \"error\" || severity === \"warning\" ? { ...problem, severity } : problem;\n });\n\n for (const file of files) {\n let source: string;\n\n try {\n source = readFileSync(path.join(cwd, file), \"utf8\");\n } catch {\n continue;\n }\n\n problems.push(...checkFileConventions(file, source, cwd, options, apiRouteFacts));\n }\n\n return problems;\n}\n\n/** Optional JSON configuration; malformed configuration is reported by the check step. */\nexport function readConventionOptions(cwd: string): ConventionOptions {\n const file = path.join(cwd, \".warlock-check.json\");\n if (!existsSync(file)) return {};\n const value: unknown = JSON.parse(readFileSync(file, \"utf8\"));\n if (!value || typeof value !== \"object\")\n throw new Error(\".warlock-check.json must be an object.\");\n const limit: unknown = Reflect.get(value, \"componentLineLimit\");\n if (limit !== undefined && (typeof limit !== \"number\" || !Number.isInteger(limit) || limit < 1))\n throw new Error(\"componentLineLimit must be a positive integer.\");\n const rules: unknown = Reflect.get(value, \"rules\");\n const overrides: NonNullable<ConventionOptions[\"rules\"]> = {};\n if (rules !== undefined) {\n if (!rules || typeof rules !== \"object\" || Array.isArray(rules))\n throw new Error(\"rules must be an object.\");\n for (const [code, severity] of Object.entries(rules)) {\n if (\n !isWarlockErrorCode(code) ||\n (severity !== \"off\" && severity !== \"warning\" && severity !== \"error\")\n )\n throw new Error(\"rules must map registered WL codes to off, warning or error.\");\n overrides[code] = severity;\n }\n }\n return {\n ...(typeof limit === \"number\" ? { componentLineLimit: limit } : {}),\n rules: overrides,\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"run-check.d.mts","names":[],"sources":["../../../../../../../../../packages/core/src/tooling/check/run-check.ts"],"mappings":";;;;;;YAgCY,mBACV,SAAS,qBACN,QAAQ,mBAAmB;;YAGpB;;EAEV;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA,gBAAgB;;EAEhB;;EAEA,MAAM;;EAEN,YAAY;;EAEZ,SAAS;;EAET;;EAEA;;;;;;;;;;;;wBA4BoB,SAAS,UAAS,eAAoB,QAAQ"}
1
+ {"version":3,"file":"run-check.d.mts","names":[],"sources":["../../../../../../../../../packages/core/src/tooling/check/run-check.ts"],"mappings":";;;;;;YAiCY,mBACV,SAAS,qBACN,QAAQ,mBAAmB;;YAGpB;;EAEV;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA,gBAAgB;;EAEhB;;EAEA,MAAM;;EAEN,YAAY;;EAEZ,SAAS;;EAET;;EAEA;;;;;;;;;;;;wBA4BoB,SAAS,UAAS,eAAoB,QAAQ"}
@@ -1,6 +1,7 @@
1
1
  import { CHECK_STEPS, checkExitCode } from "./check-report.types.mjs";
2
2
  import { createProblem } from "../problem.mjs";
3
3
  import { applyConventionFixes } from "./fixes.mjs";
4
+ import { collectApiRouteFacts } from "./api-route-references.mjs";
4
5
  import { runConventions } from "./conventions-step.mjs";
5
6
  import { listSourceFiles, listTestFiles } from "./source-files.mjs";
6
7
  import { affectedFiles } from "./affected-files.mjs";
@@ -106,7 +107,8 @@ async function runCheck(options = {}) {
106
107
  });
107
108
  const targets = async () => files ?? await listSourceFiles(cwd);
108
109
  if (options.fix && !filesBlocked && !skip.has("conventions")) try {
109
- applyConventionFixes(cwd, runConventions(cwd, await targets(), options.project));
110
+ const fixTargets = await targets();
111
+ applyConventionFixes(cwd, runConventions(cwd, fixTargets, options.project, collectApiRouteFacts(cwd, await listSourceFiles(cwd))));
110
112
  } catch (error) {
111
113
  problems.push(createProblem({
112
114
  code: "WL8007",
@@ -182,7 +184,7 @@ async function runCheck(options = {}) {
182
184
  const conventionFiles = await targets();
183
185
  return {
184
186
  detail: `${conventionFiles.length} file(s) checked`,
185
- problems: runConventions(cwd, conventionFiles, options.project)
187
+ problems: runConventions(cwd, conventionFiles, options.project, collectApiRouteFacts(cwd, await listSourceFiles(cwd)))
186
188
  };
187
189
  });
188
190
  steps.sort((first, second) => CHECK_STEPS.indexOf(first.name) - CHECK_STEPS.indexOf(second.name));
@@ -1 +1 @@
1
- {"version":3,"file":"run-check.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/tooling/check/run-check.ts"],"sourcesContent":["import { affectedFiles } from \"./affected-files\";\nimport { applyConventionFixes } from \"./fixes\";\nimport path from \"node:path\";\nimport { createProblem, type ToolingProblem } from \"../problem\";\nimport {\n runTypecheck,\n type TypecheckOptions,\n type TypecheckResult,\n} from \"../typecheck/run-typecheck\";\nimport { type GitRunner, listChangedFiles } from \"./changed-files\";\nimport {\n CHECK_STEPS,\n type CheckReport,\n type CheckStep,\n type CheckStepName,\n type CheckStepStatus,\n checkExitCode,\n} from \"./check-report.types\";\nimport { runConventions } from \"./conventions-step\";\nimport { checkNodeVersion } from \"./environment-step\";\nimport { runLint } from \"./lint-step\";\nimport { listSourceFiles, listTestFiles } from \"./source-files\";\nimport {\n findVitest,\n parseVitestReport,\n runVitest,\n type VitestRunner,\n vitestArguments,\n vitestRunFailure,\n} from \"./tests-step\";\n\n/** Runs the typecheck step. The CLI swaps in a child process so a heap overflow cannot end `check`. */\nexport type TypecheckRunner = (\n options: TypecheckOptions,\n) => Promise<TypecheckResult> | TypecheckResult;\n\n/** Inputs of {@link runCheck}. */\nexport type CheckOptions = {\n /** Project root. Default: `process.cwd()`. */\n cwd?: string;\n /** Oxlint is the only supported linter. */\n lint?: \"oxlint\";\n /** Limit every step to the files changed against `base` (default HEAD) plus untracked files. */\n changed?: boolean;\n /** Apply safe convention fixes before checking. */\n fix?: boolean;\n /** Compare with `git merge-base <base> HEAD` instead of HEAD. Implies `changed`. */\n base?: string;\n /** Steps not to run. */\n skip?: readonly CheckStepName[];\n /** tsconfig path, relative to `cwd`. Default: `tsconfig.json`. */\n project?: string;\n /** How git is run. */\n git?: GitRunner;\n /** How the typecheck runs. Default: in this process. */\n typecheck?: TypecheckRunner;\n /** How vitest runs. */\n vitest?: VitestRunner;\n /** Where the app's vitest CLI is. Default: resolved from the app root. */\n vitestBinary?: string | null;\n /** The Node.js version to check. Default: the running one. */\n nodeVersion?: string;\n};\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction statusOf(problems: readonly ToolingProblem[]): CheckStepStatus {\n const errors = problems.filter((problem) => problem.severity === \"error\");\n\n if (errors.some((problem) => problem.category === \"environment\")) return \"blocked\";\n\n return errors.length > 0 ? \"failed\" : \"passed\";\n}\n\n/** Bundles one step's timing, status and problems. */\ntype StepOutcome = { status?: CheckStepStatus; detail: string | null; problems: ToolingProblem[] };\n\n/**\n * The single verify command: typecheck, lint, the tests affected by the\n * change, and the framework's convention checks, as one report with one entry\n * per problem and an exit code that tells \"your code\" (1) from \"this machine\"\n * (2).\n *\n * Every step runs even when an earlier one failed, so one run lists every\n * problem. The only exception: with `changed`, a git failure leaves no file\n * list, and every step after `environment` is `blocked`.\n */\nexport async function runCheck(options: CheckOptions = {}): Promise<CheckReport> {\n const selection: unknown = options.lint;\n if (selection !== undefined && selection !== \"oxlint\")\n throw new Error(\n \"ESLint support was removed in 5.34.0. Oxlint/Oxfmt is the default; run npx warlock upgrade --from 5.33 --to 5.34.0.\",\n );\n const cwd = path.resolve(options.cwd ?? process.cwd());\n const changed = options.changed === true || options.base !== undefined;\n const skip = new Set(options.skip ?? []);\n const steps: CheckStep[] = [];\n const problems: ToolingProblem[] = [];\n let files: string[] | null = null;\n let filesBlocked = false;\n\n const run = async (name: CheckStepName, body: () => Promise<StepOutcome> | StepOutcome) => {\n const startedAt = Date.now();\n\n if (skip.has(name)) {\n steps.push({ name, status: \"skipped\", durationMs: 0, detail: \"skipped with --skip\" });\n return;\n }\n\n if (filesBlocked && name !== \"environment\") {\n steps.push({\n name,\n status: \"blocked\",\n durationMs: 0,\n detail: \"the changed files could not be listed\",\n });\n return;\n }\n\n let outcome: StepOutcome;\n\n try {\n outcome = await body();\n } catch (error) {\n outcome = {\n detail: null,\n problems: [\n createProblem({\n code: \"WL8007\",\n source: \"environment\",\n message: `The ${name} step crashed: ${errorMessage(error)}`,\n }),\n ],\n };\n }\n\n problems.push(...outcome.problems);\n steps.push({\n name,\n status: outcome.status ?? statusOf(outcome.problems),\n durationMs: Date.now() - startedAt,\n detail: outcome.detail,\n });\n };\n\n await run(\"environment\", async () => {\n const found = checkNodeVersion(cwd, options.nodeVersion);\n\n if (changed) {\n const listed = await listChangedFiles({\n cwd,\n base: options.base,\n git: options.git,\n includeDeleted: true,\n });\n\n if (\"problem\" in listed) {\n filesBlocked = true;\n found.push(listed.problem);\n } else {\n filesBlocked = true;\n files = await affectedFiles(cwd, listed.files, options.project);\n filesBlocked = false;\n }\n }\n\n return {\n detail: `Node ${options.nodeVersion ?? process.versions.node}${files ? `, ${files.length} changed file(s)` : \"\"}`,\n problems: found,\n };\n });\n\n const targets = async (): Promise<string[]> => files ?? (await listSourceFiles(cwd));\n\n if (options.fix && !filesBlocked && !skip.has(\"conventions\")) {\n try {\n applyConventionFixes(cwd, runConventions(cwd, await targets(), options.project));\n } catch (error) {\n problems.push(\n createProblem({\n code: \"WL8007\",\n source: \"environment\",\n message: `Could not apply fixes: ${errorMessage(error)}`,\n }),\n );\n }\n }\n\n await run(\"typecheck\", async () => {\n if (files !== null && files.length === 0) {\n return { status: \"skipped\", detail: \"no changed files\", problems: [] };\n }\n\n const typecheck = options.typecheck ?? runTypecheck;\n const result = await typecheck({\n cwd,\n project: options.project,\n ...(files !== null ? { files } : {}),\n });\n\n return {\n detail: `${result.checkedFiles} file(s) checked in ${result.durationMs}ms`,\n problems: result.problems,\n };\n });\n\n await run(\"lint\", async () => {\n const lintFiles = await targets();\n\n if (lintFiles.length === 0) {\n return { status: \"skipped\", detail: \"no files to lint\", problems: [] };\n }\n\n const { linters, problems: found } = await runLint(cwd, lintFiles);\n\n if (linters.length === 0) {\n return {\n status: \"skipped\",\n detail: \"no Oxlint config\",\n problems: [],\n };\n }\n\n return { detail: `${linters.join(\" + \")} over ${lintFiles.length} file(s)`, problems: found };\n });\n\n await run(\"tests\", async () => {\n if (files !== null && files.length === 0) {\n return { status: \"skipped\", detail: \"no changed files\", problems: [] };\n }\n\n const binary = options.vitestBinary === undefined ? findVitest(cwd) : options.vitestBinary;\n\n if (!binary) {\n if ((await listTestFiles(cwd)).length === 0) {\n return { status: \"skipped\", detail: \"no test files\", problems: [] };\n }\n\n return {\n detail: null,\n problems: [\n createProblem({\n code: \"WL8002\",\n source: \"environment\",\n message: \"The project has test files but vitest is not installed in the app.\",\n }),\n ],\n };\n }\n\n const vitest = options.vitest ?? runVitest;\n const outcome = await vitest(binary, vitestArguments(files), cwd);\n const found =\n outcome.report === undefined\n ? vitestRunFailure(outcome)\n : parseVitestReport(outcome.report, cwd);\n const total =\n typeof outcome.report === \"object\" && outcome.report !== null\n ? Reflect.get(outcome.report, \"numTotalTests\")\n : undefined;\n\n return {\n detail: typeof total === \"number\" ? `${total} test(s) ran` : null,\n problems: found,\n };\n });\n\n await run(\"conventions\", async () => {\n const conventionFiles = await targets();\n\n return {\n detail: `${conventionFiles.length} file(s) checked`,\n problems: runConventions(cwd, conventionFiles, options.project),\n };\n });\n\n // Keep the declared order even if a future step is added out of sequence.\n steps.sort((first, second) => CHECK_STEPS.indexOf(first.name) - CHECK_STEPS.indexOf(second.name));\n\n const exitCode = checkExitCode(problems);\n const errors = problems.filter((problem) => problem.severity === \"error\");\n\n return {\n schemaVersion: 1,\n ok: exitCode === 0,\n exitCode,\n changed,\n files,\n steps,\n problems,\n summary: {\n errors: errors.length,\n warnings: problems.length - errors.length,\n code: errors.filter((problem) => problem.category === \"code\").length,\n environment: errors.filter((problem) => problem.category === \"environment\").length,\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"run-check.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/tooling/check/run-check.ts"],"sourcesContent":["import { affectedFiles } from \"./affected-files\";\nimport { applyConventionFixes } from \"./fixes\";\nimport path from \"node:path\";\nimport { createProblem, type ToolingProblem } from \"../problem\";\nimport {\n runTypecheck,\n type TypecheckOptions,\n type TypecheckResult,\n} from \"../typecheck/run-typecheck\";\nimport { type GitRunner, listChangedFiles } from \"./changed-files\";\nimport {\n CHECK_STEPS,\n type CheckReport,\n type CheckStep,\n type CheckStepName,\n type CheckStepStatus,\n checkExitCode,\n} from \"./check-report.types\";\nimport { runConventions } from \"./conventions-step\";\nimport { collectApiRouteFacts } from \"./api-route-references\";\nimport { checkNodeVersion } from \"./environment-step\";\nimport { runLint } from \"./lint-step\";\nimport { listSourceFiles, listTestFiles } from \"./source-files\";\nimport {\n findVitest,\n parseVitestReport,\n runVitest,\n type VitestRunner,\n vitestArguments,\n vitestRunFailure,\n} from \"./tests-step\";\n\n/** Runs the typecheck step. The CLI swaps in a child process so a heap overflow cannot end `check`. */\nexport type TypecheckRunner = (\n options: TypecheckOptions,\n) => Promise<TypecheckResult> | TypecheckResult;\n\n/** Inputs of {@link runCheck}. */\nexport type CheckOptions = {\n /** Project root. Default: `process.cwd()`. */\n cwd?: string;\n /** Oxlint is the only supported linter. */\n lint?: \"oxlint\";\n /** Limit every step to the files changed against `base` (default HEAD) plus untracked files. */\n changed?: boolean;\n /** Apply safe convention fixes before checking. */\n fix?: boolean;\n /** Compare with `git merge-base <base> HEAD` instead of HEAD. Implies `changed`. */\n base?: string;\n /** Steps not to run. */\n skip?: readonly CheckStepName[];\n /** tsconfig path, relative to `cwd`. Default: `tsconfig.json`. */\n project?: string;\n /** How git is run. */\n git?: GitRunner;\n /** How the typecheck runs. Default: in this process. */\n typecheck?: TypecheckRunner;\n /** How vitest runs. */\n vitest?: VitestRunner;\n /** Where the app's vitest CLI is. Default: resolved from the app root. */\n vitestBinary?: string | null;\n /** The Node.js version to check. Default: the running one. */\n nodeVersion?: string;\n};\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction statusOf(problems: readonly ToolingProblem[]): CheckStepStatus {\n const errors = problems.filter((problem) => problem.severity === \"error\");\n\n if (errors.some((problem) => problem.category === \"environment\")) return \"blocked\";\n\n return errors.length > 0 ? \"failed\" : \"passed\";\n}\n\n/** Bundles one step's timing, status and problems. */\ntype StepOutcome = { status?: CheckStepStatus; detail: string | null; problems: ToolingProblem[] };\n\n/**\n * The single verify command: typecheck, lint, the tests affected by the\n * change, and the framework's convention checks, as one report with one entry\n * per problem and an exit code that tells \"your code\" (1) from \"this machine\"\n * (2).\n *\n * Every step runs even when an earlier one failed, so one run lists every\n * problem. The only exception: with `changed`, a git failure leaves no file\n * list, and every step after `environment` is `blocked`.\n */\nexport async function runCheck(options: CheckOptions = {}): Promise<CheckReport> {\n const selection: unknown = options.lint;\n if (selection !== undefined && selection !== \"oxlint\")\n throw new Error(\n \"ESLint support was removed in 5.34.0. Oxlint/Oxfmt is the default; run npx warlock upgrade --from 5.33 --to 5.34.0.\",\n );\n const cwd = path.resolve(options.cwd ?? process.cwd());\n const changed = options.changed === true || options.base !== undefined;\n const skip = new Set(options.skip ?? []);\n const steps: CheckStep[] = [];\n const problems: ToolingProblem[] = [];\n let files: string[] | null = null;\n let filesBlocked = false;\n\n const run = async (name: CheckStepName, body: () => Promise<StepOutcome> | StepOutcome) => {\n const startedAt = Date.now();\n\n if (skip.has(name)) {\n steps.push({ name, status: \"skipped\", durationMs: 0, detail: \"skipped with --skip\" });\n return;\n }\n\n if (filesBlocked && name !== \"environment\") {\n steps.push({\n name,\n status: \"blocked\",\n durationMs: 0,\n detail: \"the changed files could not be listed\",\n });\n return;\n }\n\n let outcome: StepOutcome;\n\n try {\n outcome = await body();\n } catch (error) {\n outcome = {\n detail: null,\n problems: [\n createProblem({\n code: \"WL8007\",\n source: \"environment\",\n message: `The ${name} step crashed: ${errorMessage(error)}`,\n }),\n ],\n };\n }\n\n problems.push(...outcome.problems);\n steps.push({\n name,\n status: outcome.status ?? statusOf(outcome.problems),\n durationMs: Date.now() - startedAt,\n detail: outcome.detail,\n });\n };\n\n await run(\"environment\", async () => {\n const found = checkNodeVersion(cwd, options.nodeVersion);\n\n if (changed) {\n const listed = await listChangedFiles({\n cwd,\n base: options.base,\n git: options.git,\n includeDeleted: true,\n });\n\n if (\"problem\" in listed) {\n filesBlocked = true;\n found.push(listed.problem);\n } else {\n filesBlocked = true;\n files = await affectedFiles(cwd, listed.files, options.project);\n filesBlocked = false;\n }\n }\n\n return {\n detail: `Node ${options.nodeVersion ?? process.versions.node}${files ? `, ${files.length} changed file(s)` : \"\"}`,\n problems: found,\n };\n });\n\n const targets = async (): Promise<string[]> => files ?? (await listSourceFiles(cwd));\n\n if (options.fix && !filesBlocked && !skip.has(\"conventions\")) {\n try {\n const fixTargets = await targets();\n applyConventionFixes(\n cwd,\n runConventions(\n cwd,\n fixTargets,\n options.project,\n collectApiRouteFacts(cwd, await listSourceFiles(cwd)),\n ),\n );\n } catch (error) {\n problems.push(\n createProblem({\n code: \"WL8007\",\n source: \"environment\",\n message: `Could not apply fixes: ${errorMessage(error)}`,\n }),\n );\n }\n }\n\n await run(\"typecheck\", async () => {\n if (files !== null && files.length === 0) {\n return { status: \"skipped\", detail: \"no changed files\", problems: [] };\n }\n\n const typecheck = options.typecheck ?? runTypecheck;\n const result = await typecheck({\n cwd,\n project: options.project,\n ...(files !== null ? { files } : {}),\n });\n\n return {\n detail: `${result.checkedFiles} file(s) checked in ${result.durationMs}ms`,\n problems: result.problems,\n };\n });\n\n await run(\"lint\", async () => {\n const lintFiles = await targets();\n\n if (lintFiles.length === 0) {\n return { status: \"skipped\", detail: \"no files to lint\", problems: [] };\n }\n\n const { linters, problems: found } = await runLint(cwd, lintFiles);\n\n if (linters.length === 0) {\n return {\n status: \"skipped\",\n detail: \"no Oxlint config\",\n problems: [],\n };\n }\n\n return { detail: `${linters.join(\" + \")} over ${lintFiles.length} file(s)`, problems: found };\n });\n\n await run(\"tests\", async () => {\n if (files !== null && files.length === 0) {\n return { status: \"skipped\", detail: \"no changed files\", problems: [] };\n }\n\n const binary = options.vitestBinary === undefined ? findVitest(cwd) : options.vitestBinary;\n\n if (!binary) {\n if ((await listTestFiles(cwd)).length === 0) {\n return { status: \"skipped\", detail: \"no test files\", problems: [] };\n }\n\n return {\n detail: null,\n problems: [\n createProblem({\n code: \"WL8002\",\n source: \"environment\",\n message: \"The project has test files but vitest is not installed in the app.\",\n }),\n ],\n };\n }\n\n const vitest = options.vitest ?? runVitest;\n const outcome = await vitest(binary, vitestArguments(files), cwd);\n const found =\n outcome.report === undefined\n ? vitestRunFailure(outcome)\n : parseVitestReport(outcome.report, cwd);\n const total =\n typeof outcome.report === \"object\" && outcome.report !== null\n ? Reflect.get(outcome.report, \"numTotalTests\")\n : undefined;\n\n return {\n detail: typeof total === \"number\" ? `${total} test(s) ran` : null,\n problems: found,\n };\n });\n\n await run(\"conventions\", async () => {\n const conventionFiles = await targets();\n\n return {\n detail: `${conventionFiles.length} file(s) checked`,\n // Facts come from the whole project even when only changed files are checked:\n // a route registered in one file may be referenced from another.\n problems: runConventions(\n cwd,\n conventionFiles,\n options.project,\n collectApiRouteFacts(cwd, await listSourceFiles(cwd)),\n ),\n };\n });\n\n // Keep the declared order even if a future step is added out of sequence.\n steps.sort((first, second) => CHECK_STEPS.indexOf(first.name) - CHECK_STEPS.indexOf(second.name));\n\n const exitCode = checkExitCode(problems);\n const errors = problems.filter((problem) => problem.severity === \"error\");\n\n return {\n schemaVersion: 1,\n ok: exitCode === 0,\n exitCode,\n changed,\n files,\n steps,\n problems,\n summary: {\n errors: errors.length,\n warnings: problems.length - errors.length,\n code: errors.filter((problem) => problem.category === \"code\").length,\n environment: errors.filter((problem) => problem.category === \"environment\").length,\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import { scanTranslationSource } from "../../localization/translation-usage.mjs";
2
+ import { createProblem } from "../problem.mjs";
3
+
4
+ //#region ../../packages/core/src/tooling/check/translation-conventions.ts
5
+ /** Application code that renders on the client: everything under `src/web` except setup and test files. */
6
+ const WEB_CLIENT_FILE = /^src\/web\/.+\.[cm]?[jt]sx?$/;
7
+ const SETUP_OR_TEST_FILE = /\.(?:setup|test|spec)\.[cm]?[jt]sx?$/i;
8
+ /**
9
+ * `WL9104`: a translation lookup whose key is only known at runtime.
10
+ *
11
+ * A page ships the translation keys its client code can reach, found by
12
+ * reading the code. A key computed at runtime (`trans(key)`, a translator
13
+ * passed on as a value) defeats that reading, so the page falls back to every
14
+ * group. The fix is a `config.translations` declaration in the page's setup
15
+ * file, which this rule points at.
16
+ */
17
+ function translationProblems(relativePath, source) {
18
+ if (!WEB_CLIENT_FILE.test(relativePath) || SETUP_OR_TEST_FILE.test(relativePath)) return [];
19
+ return scanTranslationSource(relativePath, source).usage.dynamicSites.map((site) => createProblem({
20
+ code: "WL9104",
21
+ source: "convention",
22
+ severity: "warning",
23
+ message: `Translation key \`${site.text}\` is computed at runtime, so a page that renders it ships every translation group. Declare the keys it uses with \`config.translations\` in the page's setup file.`,
24
+ file: relativePath,
25
+ line: site.line,
26
+ column: site.column
27
+ }));
28
+ }
29
+
30
+ //#endregion
31
+ export { translationProblems };
32
+ //# sourceMappingURL=translation-conventions.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translation-conventions.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/tooling/check/translation-conventions.ts"],"sourcesContent":["import { scanTranslationSource } from \"../../localization/translation-usage\";\nimport { createProblem, type ToolingProblem } from \"../problem\";\n\n/** Application code that renders on the client: everything under `src/web` except setup and test files. */\nconst WEB_CLIENT_FILE = /^src\\/web\\/.+\\.[cm]?[jt]sx?$/;\nconst SETUP_OR_TEST_FILE = /\\.(?:setup|test|spec)\\.[cm]?[jt]sx?$/i;\n\n/**\n * `WL9104`: a translation lookup whose key is only known at runtime.\n *\n * A page ships the translation keys its client code can reach, found by\n * reading the code. A key computed at runtime (`trans(key)`, a translator\n * passed on as a value) defeats that reading, so the page falls back to every\n * group. The fix is a `config.translations` declaration in the page's setup\n * file, which this rule points at.\n */\nexport function translationProblems(relativePath: string, source: string): ToolingProblem[] {\n if (!WEB_CLIENT_FILE.test(relativePath) || SETUP_OR_TEST_FILE.test(relativePath)) return [];\n\n return scanTranslationSource(relativePath, source).usage.dynamicSites.map((site) =>\n createProblem({\n code: \"WL9104\",\n source: \"convention\",\n severity: \"warning\",\n message:\n `Translation key \\`${site.text}\\` is computed at runtime, so a page that renders it ships every ` +\n \"translation group. Declare the keys it uses with `config.translations` in the page's setup file.\",\n file: relativePath,\n line: site.line,\n column: site.column,\n }),\n );\n}\n"],"mappings":";;;;;AAIA,MAAM,kBAAkB;AACxB,MAAM,qBAAqB;;;;;;;;;;AAW3B,SAAgB,oBAAoB,cAAsB,QAAkC;CAC1F,IAAI,CAAC,gBAAgB,KAAK,YAAY,KAAK,mBAAmB,KAAK,YAAY,GAAG,OAAO,CAAC;CAE1F,OAAO,sBAAsB,cAAc,MAAM,CAAC,CAAC,MAAM,aAAa,KAAK,SACzE,cAAc;EACZ,MAAM;EACN,QAAQ;EACR,UAAU;EACV,SACE,qBAAqB,KAAK,KAAK;EAEjC,MAAM;EACN,MAAM,KAAK;EACX,QAAQ,KAAK;CACf,CAAC,CACH;AACF"}
@@ -1,5 +1,5 @@
1
- import { isConfigFile, isEventFile, isLocaleFile, isMainFile, isRouteFile, isWorkerFile } from "../../dev-server/special-file-patterns.mjs";
2
1
  import { forEachChild } from "../syntax/ast.mjs";
2
+ import { isConfigFile, isEventFile, isLocaleFile, isMainFile, isRouteFile, isWorkerFile } from "../../dev-server/special-file-patterns.mjs";
3
3
  import path from "node:path";
4
4
  import { existsSync, readFileSync, readdirSync } from "node:fs";
5
5
  import { readTsconfig, resolvePathAlias } from "get-tsconfig";
@@ -4,6 +4,7 @@ import { loggerSyncCalls } from "./logger-sync-calls.mjs";
4
4
  import { node2215, warlockVersions } from "./package-json.mjs";
5
5
  import { oxcToolchain, oxcToolchainRemoval } from "./oxc-toolchain.mjs";
6
6
  import { requestLogSnapshot } from "./request-log-snapshot.mjs";
7
+ import { viteVitest } from "./vite-vitest.mjs";
7
8
  import { aiAnthropicBody, aiCompactionOptIn, aiFinishReason, aiToolDispatch, rateLimitDefaults, sealContainerCatch, sealRefineOutput, sealUnionOutput, sealValidateResult } from "./review-sites.mjs";
8
9
 
9
10
  //#region ../../packages/core/src/upgrade/codemods/index.ts
@@ -29,6 +30,7 @@ const CODEMODS = [
29
30
  aiCompactionOptIn,
30
31
  oxcToolchain,
31
32
  oxcToolchainRemoval,
33
+ viteVitest,
32
34
  warlockVersions
33
35
  ];
34
36
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/upgrade/codemods/index.ts"],"sourcesContent":["import type { Codemod } from \"../types\";\nimport { headViewport } from \"./head-viewport\";\nimport { loggerSyncCalls } from \"./logger-sync-calls\";\nimport { node2215, warlockVersions } from \"./package-json\";\nimport { oxcToolchain, oxcToolchainRemoval } from \"./oxc-toolchain\";\nimport { requestLogSnapshot } from \"./request-log-snapshot\";\nimport {\n aiAnthropicBody,\n aiCompactionOptIn,\n aiFinishReason,\n aiToolDispatch,\n rateLimitDefaults,\n sealContainerCatch,\n sealRefineOutput,\n sealUnionOutput,\n sealValidateResult,\n} from \"./review-sites\";\n\n/**\n * Every codemod, oldest release first. A codemod runs when the app upgrades\n * across its `version` (`from < version <= to`); `\"target\"` ones run on every\n * upgrade. Add one here for each breaking change, and name its id next to the\n * `**BREAKING**` entry in the package changelog.\n */\nexport const CODEMODS: readonly Codemod[] = [\n loggerSyncCalls,\n requestLogSnapshot,\n headViewport,\n sealValidateResult,\n aiToolDispatch,\n aiAnthropicBody,\n aiFinishReason,\n node2215,\n rateLimitDefaults,\n sealRefineOutput,\n sealContainerCatch,\n sealUnionOutput,\n aiCompactionOptIn,\n oxcToolchain,\n oxcToolchainRemoval,\n warlockVersions,\n];\n\nexport * from \"./helpers\";\n"],"mappings":";;;;;;;;;;;;;;;AAwBA,MAAa,WAA+B;CAC1C;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/upgrade/codemods/index.ts"],"sourcesContent":["import type { Codemod } from \"../types\";\nimport { headViewport } from \"./head-viewport\";\nimport { loggerSyncCalls } from \"./logger-sync-calls\";\nimport { node2215, warlockVersions } from \"./package-json\";\nimport { oxcToolchain, oxcToolchainRemoval } from \"./oxc-toolchain\";\nimport { requestLogSnapshot } from \"./request-log-snapshot\";\nimport { viteVitest } from \"./vite-vitest\";\nimport {\n aiAnthropicBody,\n aiCompactionOptIn,\n aiFinishReason,\n aiToolDispatch,\n rateLimitDefaults,\n sealContainerCatch,\n sealRefineOutput,\n sealUnionOutput,\n sealValidateResult,\n} from \"./review-sites\";\n\n/**\n * Every codemod, oldest release first. A codemod runs when the app upgrades\n * across its `version` (`from < version <= to`); `\"target\"` ones run on every\n * upgrade. Add one here for each breaking change, and name its id next to the\n * `**BREAKING**` entry in the package changelog.\n */\nexport const CODEMODS: readonly Codemod[] = [\n loggerSyncCalls,\n requestLogSnapshot,\n headViewport,\n sealValidateResult,\n aiToolDispatch,\n aiAnthropicBody,\n aiFinishReason,\n node2215,\n rateLimitDefaults,\n sealRefineOutput,\n sealContainerCatch,\n sealUnionOutput,\n aiCompactionOptIn,\n oxcToolchain,\n oxcToolchainRemoval,\n viteVitest,\n warlockVersions,\n];\n\nexport * from \"./helpers\";\n"],"mappings":";;;;;;;;;;;;;;;;AAyBA,MAAa,WAA+B;CAC1C;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF"}