@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.
- package/CHANGELOG.md +25 -0
- package/esm/dev-server/module-loader.mjs +1 -1
- package/esm/dev-server/parse-imports.mjs +1 -1
- package/esm/dev-server/type-generator.mjs +1 -1
- package/esm/e2e/cdp.d.mts.map +1 -1
- package/esm/e2e/cdp.mjs +9 -2
- package/esm/e2e/cdp.mjs.map +1 -1
- package/esm/e2e/page.d.mts +9 -1
- package/esm/e2e/page.d.mts.map +1 -1
- package/esm/e2e/page.mjs +45 -7
- package/esm/e2e/page.mjs.map +1 -1
- package/esm/e2e/types.d.mts +18 -0
- package/esm/e2e/types.d.mts.map +1 -1
- package/esm/e2e/types.mjs.map +1 -1
- package/esm/errors/error-codes.d.mts +24 -0
- package/esm/errors/error-codes.d.mts.map +1 -1
- package/esm/errors/error-codes.mjs +24 -0
- package/esm/errors/error-codes.mjs.map +1 -1
- package/esm/generations/features/test.feature.mjs +7 -7
- package/esm/generations/features/test.feature.mjs.map +1 -1
- package/esm/generations/features/web.feature.mjs +3 -3
- package/esm/generations/features/web.feature.mjs.map +1 -1
- package/esm/index.d.mts +4 -2
- package/esm/index.mjs +3 -1
- package/esm/localization/index.mjs +3 -1
- package/esm/localization/translation-usage-graph.d.mts +49 -0
- package/esm/localization/translation-usage-graph.d.mts.map +1 -0
- package/esm/localization/translation-usage-graph.mjs +128 -0
- package/esm/localization/translation-usage-graph.mjs.map +1 -0
- package/esm/localization/translation-usage.d.mts +85 -0
- package/esm/localization/translation-usage.d.mts.map +1 -0
- package/esm/localization/translation-usage.mjs +307 -0
- package/esm/localization/translation-usage.mjs.map +1 -0
- package/esm/production/build-budgets.d.mts +18 -0
- package/esm/production/build-budgets.d.mts.map +1 -1
- package/esm/production/build-budgets.mjs.map +1 -1
- package/esm/production/route-registration-snapshot.mjs +5 -3
- package/esm/production/route-registration-snapshot.mjs.map +1 -1
- package/esm/router/router.d.mts +2 -0
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +4 -1
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts +19 -0
- package/esm/router/types.d.mts.map +1 -1
- package/esm/tooling/check/api-route-references.d.mts +16 -0
- package/esm/tooling/check/api-route-references.d.mts.map +1 -0
- package/esm/tooling/check/api-route-references.mjs +146 -0
- package/esm/tooling/check/api-route-references.mjs.map +1 -0
- package/esm/tooling/check/conventions-step.d.mts +6 -3
- package/esm/tooling/check/conventions-step.d.mts.map +1 -1
- package/esm/tooling/check/conventions-step.mjs +19 -10
- package/esm/tooling/check/conventions-step.mjs.map +1 -1
- package/esm/tooling/check/run-check.d.mts.map +1 -1
- package/esm/tooling/check/run-check.mjs +4 -2
- package/esm/tooling/check/run-check.mjs.map +1 -1
- package/esm/tooling/check/translation-conventions.mjs +32 -0
- package/esm/tooling/check/translation-conventions.mjs.map +1 -0
- package/esm/tooling/inspect/collect-files.mjs +1 -1
- package/esm/upgrade/codemods/index.mjs +2 -0
- package/esm/upgrade/codemods/index.mjs.map +1 -1
- package/esm/upgrade/codemods/vite-vitest.mjs +170 -0
- package/esm/upgrade/codemods/vite-vitest.mjs.map +1 -0
- package/esm/upgrade/notes.mjs +4 -0
- package/esm/upgrade/notes.mjs.map +1 -1
- package/esm/warlock-config/index.d.mts +2 -2
- package/llms-full.txt +127 -16
- package/package.json +13 -13
- package/skills/register-route/SKILL.md +13 -0
- package/skills/test-service/SKILL.md +8 -8
- package/skills/upgrade-from-5-29/SKILL.md +91 -0
- package/skills/use-localization/SKILL.md +4 -0
- 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":"
|
|
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 = [
|
|
134
|
-
...
|
|
135
|
-
...
|
|
136
|
-
...
|
|
137
|
-
|
|
138
|
-
|
|
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":";;;;;;
|
|
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
|
-
|
|
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":"
|
|
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"}
|