@warlock.js/core 5.34.3 → 5.35.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 +44 -0
- package/esm/cli/cli-commands.manager.mjs +1 -1
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/commands/add.command.mjs +1 -0
- package/esm/cli/commands/add.command.mjs.map +1 -1
- package/esm/cli/commands/build.command.mjs +30 -3
- package/esm/cli/commands/build.command.mjs.map +1 -1
- package/esm/cli/commands/check/check-command.action.mjs +1 -0
- package/esm/cli/commands/check/check-command.action.mjs.map +1 -1
- package/esm/cli/commands/check.command.mjs +5 -0
- package/esm/cli/commands/check.command.mjs.map +1 -1
- package/esm/cli/commands/explain/explain-command.action.mjs +21 -0
- package/esm/cli/commands/explain/explain-command.action.mjs.map +1 -1
- package/esm/cli/commands/explain.command.mjs +14 -4
- package/esm/cli/commands/explain.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generate.command.mjs +19 -1
- package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/endpoint.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/endpoint.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/web.generator.mjs +169 -0
- package/esm/cli/commands/generate/generators/web.generator.mjs.map +1 -0
- package/esm/cli/commands/generate/templates/web-stubs.mjs +231 -0
- package/esm/cli/commands/generate/templates/web-stubs.mjs.map +1 -0
- package/esm/cli/commands/inspect/inspect-command.action.mjs +4 -3
- package/esm/cli/commands/inspect/inspect-command.action.mjs.map +1 -1
- package/esm/cli/commands/inspect.command.mjs +13 -8
- package/esm/cli/commands/inspect.command.mjs.map +1 -1
- package/esm/cli/commands/mcp/mcp-command.action.mjs +1 -1
- package/esm/cli/commands/mcp/mcp-command.action.mjs.map +1 -1
- package/esm/config/config-loader.d.mts.map +1 -1
- package/esm/config/config-loader.mjs +2 -0
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/config/load-config-files.mjs +4 -0
- package/esm/config/load-config-files.mjs.map +1 -1
- package/esm/dev-server/file-event-handler.mjs +11 -1
- package/esm/dev-server/file-event-handler.mjs.map +1 -1
- package/esm/dev-server/file-manager.d.mts +1 -1
- package/esm/dev-server/file-manager.d.mts.map +1 -1
- package/esm/dev-server/file-manager.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +16 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/files-watcher.mjs +13 -4
- package/esm/dev-server/files-watcher.mjs.map +1 -1
- package/esm/dev-server/loader/loader-port-acks.mjs +21 -9
- package/esm/dev-server/loader/loader-port-acks.mjs.map +1 -1
- package/esm/dev-server/loader/register-loader.mjs +31 -12
- package/esm/dev-server/loader/register-loader.mjs.map +1 -1
- package/esm/dev-server/module-loader.mjs +14 -0
- package/esm/dev-server/module-loader.mjs.map +1 -1
- package/esm/dev-server/special-file-patterns.mjs +4 -1
- package/esm/dev-server/special-file-patterns.mjs.map +1 -1
- package/esm/e2e/index.d.mts +2 -2
- package/esm/e2e/page.d.mts +6 -1
- package/esm/e2e/page.d.mts.map +1 -1
- package/esm/e2e/page.mjs +36 -1
- package/esm/e2e/page.mjs.map +1 -1
- package/esm/e2e/types.d.mts +31 -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 +48 -0
- package/esm/errors/error-codes.d.mts.map +1 -1
- package/esm/errors/error-codes.mjs +48 -0
- package/esm/errors/error-codes.mjs.map +1 -1
- package/esm/errors/index.d.mts +2 -2
- package/esm/errors/index.mjs +2 -2
- package/esm/generations/add-command.action.mjs +2 -0
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/design-system-payload.mjs +53 -0
- package/esm/generations/design-system-payload.mjs.map +1 -0
- package/esm/generations/features/design-system.feature.mjs +38 -0
- package/esm/generations/features/design-system.feature.mjs.map +1 -0
- package/esm/generations/features/index.mjs +3 -1
- package/esm/generations/features/index.mjs.map +1 -1
- package/esm/generations/features/notifications.feature.mjs +13 -2
- package/esm/generations/features/notifications.feature.mjs.map +1 -1
- package/esm/generations/features/test.feature.mjs +48 -13
- package/esm/generations/features/test.feature.mjs.map +1 -1
- package/esm/generations/stubs.mjs +9 -5
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/error-mapper.d.mts +1 -1
- package/esm/http/error-mapper.mjs +3 -3
- package/esm/http/error-mapper.mjs.map +1 -1
- package/esm/http/error-response.d.mts +3 -0
- package/esm/http/error-response.d.mts.map +1 -1
- package/esm/http/error-response.mjs +8 -2
- package/esm/http/error-response.mjs.map +1 -1
- package/esm/http/errors/errors.d.mts +8 -0
- package/esm/http/errors/errors.d.mts.map +1 -1
- package/esm/http/errors/errors.mjs +13 -1
- package/esm/http/errors/errors.mjs.map +1 -1
- package/esm/http/index.d.mts +3 -2
- package/esm/http/index.mjs +3 -2
- package/esm/http/list-endpoint.d.mts +35 -0
- package/esm/http/list-endpoint.d.mts.map +1 -0
- package/esm/http/list-endpoint.mjs +28 -0
- package/esm/http/list-endpoint.mjs.map +1 -0
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +12 -2
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/server.mjs +1 -1
- package/esm/index.d.mts +8 -6
- package/esm/index.mjs +3 -2
- package/esm/openapi/security.mjs +12 -4
- package/esm/openapi/security.mjs.map +1 -1
- package/esm/openapi/types.d.mts +2 -0
- package/esm/openapi/types.d.mts.map +1 -1
- package/esm/production/build-budgets.d.mts +46 -0
- package/esm/production/build-budgets.d.mts.map +1 -0
- package/esm/production/build-budgets.mjs +109 -0
- package/esm/production/build-budgets.mjs.map +1 -0
- package/esm/production/production-builder.mjs +13 -1
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/resolve-build-config.d.mts +1 -1
- package/esm/production/resolve-build-config.d.mts.map +1 -1
- package/esm/production/resolve-build-config.mjs.map +1 -1
- package/esm/restful/restful.d.mts +1 -1
- package/esm/tests/fakes/storage-fake.d.mts +3 -1
- package/esm/tests/fakes/storage-fake.d.mts.map +1 -1
- package/esm/tests/fakes/storage-fake.mjs +3 -3
- package/esm/tests/fakes/storage-fake.mjs.map +1 -1
- package/esm/tests/fakes/test-fakes.d.mts +3 -1
- package/esm/tests/fakes/test-fakes.d.mts.map +1 -1
- package/esm/tests/fakes/test-fakes.mjs +3 -2
- package/esm/tests/fakes/test-fakes.mjs.map +1 -1
- package/esm/tests/index.d.mts +4 -2
- package/esm/tests/index.mjs +3 -1
- package/esm/tests/load-unit-config.mjs +24 -0
- package/esm/tests/load-unit-config.mjs.map +1 -0
- package/esm/tests/response-contract.d.mts +32 -0
- package/esm/tests/response-contract.d.mts.map +1 -0
- package/esm/tests/response-contract.mjs +116 -0
- package/esm/tests/response-contract.mjs.map +1 -0
- package/esm/tests/start-http-development-server.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.mjs +10 -2
- package/esm/tests/start-http-development-server.mjs.map +1 -1
- package/esm/tests/test-lifecycle-state.mjs.map +1 -1
- package/esm/tests/test-locales.mjs +33 -0
- package/esm/tests/test-locales.mjs.map +1 -0
- package/esm/tests/test-runtime-scope.d.mts +15 -0
- package/esm/tests/test-runtime-scope.d.mts.map +1 -0
- package/esm/tests/test-runtime-scope.mjs +80 -0
- package/esm/tests/test-runtime-scope.mjs.map +1 -0
- package/esm/tests/vitest-setup.d.mts +13 -3
- package/esm/tests/vitest-setup.d.mts.map +1 -1
- package/esm/tests/vitest-setup.mjs +146 -18
- package/esm/tests/vitest-setup.mjs.map +1 -1
- package/esm/tooling/check/affected-files.d.mts +5 -0
- package/esm/tooling/check/affected-files.d.mts.map +1 -0
- package/esm/tooling/check/affected-files.mjs +56 -0
- package/esm/tooling/check/affected-files.mjs.map +1 -0
- package/esm/tooling/check/changed-files.d.mts +2 -0
- package/esm/tooling/check/changed-files.d.mts.map +1 -1
- package/esm/tooling/check/changed-files.mjs +2 -2
- package/esm/tooling/check/changed-files.mjs.map +1 -1
- package/esm/tooling/check/conventions-step.d.mts +2 -1
- package/esm/tooling/check/conventions-step.d.mts.map +1 -1
- package/esm/tooling/check/conventions-step.mjs +48 -6
- package/esm/tooling/check/conventions-step.mjs.map +1 -1
- package/esm/tooling/check/fixes.d.mts +8 -0
- package/esm/tooling/check/fixes.d.mts.map +1 -0
- package/esm/tooling/check/fixes.mjs +31 -0
- package/esm/tooling/check/fixes.mjs.map +1 -0
- package/esm/tooling/check/index.d.mts +12 -0
- package/esm/tooling/check/index.mjs +3 -2
- package/esm/tooling/check/run-check.d.mts +2 -0
- package/esm/tooling/check/run-check.d.mts.map +1 -1
- package/esm/tooling/check/run-check.mjs +20 -4
- package/esm/tooling/check/run-check.mjs.map +1 -1
- package/esm/tooling/check/run-tests.mjs +1 -1
- package/esm/tooling/check/source-ast.d.mts +1 -0
- package/esm/tooling/check/web-conventions.d.mts +10 -0
- package/esm/tooling/check/web-conventions.d.mts.map +1 -0
- package/esm/tooling/check/web-conventions.mjs +126 -0
- package/esm/tooling/check/web-conventions.mjs.map +1 -0
- package/esm/tooling/index.d.mts +7 -2
- package/esm/tooling/index.mjs +4 -2
- package/esm/tooling/inspect/collect-files.mjs +172 -0
- package/esm/tooling/inspect/collect-files.mjs.map +1 -0
- package/esm/tooling/inspect/collect-pages.mjs +3 -2
- package/esm/tooling/inspect/collect-pages.mjs.map +1 -1
- package/esm/tooling/inspect/collect-routes.mjs +1 -1
- package/esm/tooling/inspect/collect-routes.mjs.map +1 -1
- package/esm/tooling/inspect/explain-file.d.mts +12 -0
- package/esm/tooling/inspect/explain-file.d.mts.map +1 -0
- package/esm/tooling/inspect/explain-file.mjs +45 -0
- package/esm/tooling/inspect/explain-file.mjs.map +1 -0
- package/esm/tooling/inspect/index.d.mts +3 -2
- package/esm/tooling/inspect/index.mjs +2 -1
- package/esm/tooling/inspect/inspect-app.d.mts.map +1 -1
- package/esm/tooling/inspect/inspect-app.mjs +9 -3
- package/esm/tooling/inspect/inspect-app.mjs.map +1 -1
- package/esm/tooling/inspect/inspect-report.schema.mjs +34 -1
- package/esm/tooling/inspect/inspect-report.schema.mjs.map +1 -1
- package/esm/tooling/inspect/inspect-report.types.d.mts +17 -1
- package/esm/tooling/inspect/inspect-report.types.d.mts.map +1 -1
- package/esm/tooling/inspect/inspect-report.types.mjs +2 -1
- package/esm/tooling/inspect/inspect-report.types.mjs.map +1 -1
- package/esm/tooling/mcp/mcp-tools.d.mts.map +1 -1
- package/esm/tooling/mcp/mcp-tools.mjs +8 -2
- package/esm/tooling/mcp/mcp-tools.mjs.map +1 -1
- package/esm/tooling/problem.d.mts +4 -0
- package/esm/tooling/problem.d.mts.map +1 -1
- package/esm/tooling/problem.mjs +1 -0
- package/esm/tooling/problem.mjs.map +1 -1
- package/esm/tooling/syntax/ast.d.mts +2 -0
- package/esm/tooling/typecheck/run-typecheck.mjs +5 -1
- package/esm/tooling/typecheck/run-typecheck.mjs.map +1 -1
- package/esm/vite/index.d.mts +3 -1
- package/esm/vite/index.mjs +3 -1
- package/esm/vite/test-setup-reporter.d.mts +16 -0
- package/esm/vite/test-setup-reporter.d.mts.map +1 -0
- package/esm/vite/test-setup-reporter.mjs +55 -0
- package/esm/vite/test-setup-reporter.mjs.map +1 -0
- package/esm/vite/test-workers.d.mts +5 -0
- package/esm/vite/test-workers.d.mts.map +1 -0
- package/esm/vite/test-workers.mjs +38 -0
- package/esm/vite/test-workers.mjs.map +1 -0
- package/esm/warlock-config/index.d.mts +2 -1
- package/esm/warlock-config/types.d.mts +3 -0
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/llms-full.txt +763 -9
- package/llms.txt +2 -0
- package/package.json +15 -14
- package/schemas/inspect-report.schema.json +47 -1
- package/skills/build-budgets/SKILL.md +51 -0
- package/skills/build-restful/SKILL.md +56 -0
- package/skills/configure-app/SKILL.md +7 -0
- package/skills/create-module/SKILL.md +9 -0
- package/skills/generate-web-ui/SKILL.md +71 -0
- package/skills/index.md +2 -2
- package/skills/run-app/SKILL.md +1 -1
- package/skills/send-mail/SKILL.md +25 -0
- package/skills/send-response/SKILL.md +50 -0
- package/skills/test-browser/SKILL.md +53 -0
- package/skills/test-http/SKILL.md +91 -0
- package/skills/test-service/SKILL.md +161 -3
- package/skills/upgrade-from-5-29/SKILL.md +18 -0
- package/skills/use-localization/SKILL.md +21 -0
- package/skills/warlock-check/SKILL.md +72 -1
- package/skills/warlock-inspect/SKILL.md +45 -2
- package/skills/warlock-mcp/SKILL.md +4 -0
- package/skills/write-cli-command/SKILL.md +18 -0
- package/esm/production/build-app-production.mjs +0 -14
- package/esm/production/build-app-production.mjs.map +0 -1
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { isWarlockErrorCode } from "../../errors/error-codes.mjs";
|
|
1
2
|
import { parseSourceFile } from "../syntax/ast.mjs";
|
|
2
3
|
import { importSpecifiers } from "../syntax/imports.mjs";
|
|
3
4
|
import { createProblem } from "../problem.mjs";
|
|
5
|
+
import { importFixes } from "./fixes.mjs";
|
|
4
6
|
import { positionOf } from "./source-ast.mjs";
|
|
5
7
|
import { codeStyleFindings } from "./code-style-conventions.mjs";
|
|
6
8
|
import { securityFindings } from "./security-conventions.mjs";
|
|
9
|
+
import { responseSchemaFindings, webFindings } from "./web-conventions.mjs";
|
|
7
10
|
import path from "node:path";
|
|
8
11
|
import { existsSync, readFileSync } from "node:fs";
|
|
9
12
|
import { parse } from "jsonc-parser";
|
|
@@ -62,6 +65,7 @@ function isIgnored(lines, line, code) {
|
|
|
62
65
|
/** The import conventions (`WL9001`–`WL9003`) of one parsed file. */
|
|
63
66
|
function importProblems(sourceFile, relativePath, cwd) {
|
|
64
67
|
const problems = [];
|
|
68
|
+
const fixes = importFixes(relativePath, sourceFile.text);
|
|
65
69
|
const importerModule = moduleOf(relativePath);
|
|
66
70
|
for (const { specifier, node } of importsOf(sourceFile)) {
|
|
67
71
|
const at = {
|
|
@@ -70,6 +74,7 @@ function importProblems(sourceFile, relativePath, cwd) {
|
|
|
70
74
|
};
|
|
71
75
|
if (specifier.startsWith(".") && /\.jsx?$/.test(specifier)) problems.push(createProblem({
|
|
72
76
|
code: "WL9002",
|
|
77
|
+
fix: fixes.find((fix) => fix.end === node.end - 1),
|
|
73
78
|
source: "convention",
|
|
74
79
|
message: `Import "${specifier}" ends in a .js extension.`,
|
|
75
80
|
...at
|
|
@@ -123,11 +128,19 @@ function findingProblem(sourceFile, relativePath, finding) {
|
|
|
123
128
|
* A `// warlock-check-ignore WL9006` comment on the problem's line or the line
|
|
124
129
|
* above silences that code there; without codes it silences every one.
|
|
125
130
|
*/
|
|
126
|
-
function checkFileConventions(relativePath, source, cwd) {
|
|
131
|
+
function checkFileConventions(relativePath, source, cwd, options = {}) {
|
|
127
132
|
const sourceFile = parseSourceFile(relativePath, source);
|
|
128
|
-
const problems = [...importProblems(sourceFile, relativePath, cwd), ...[
|
|
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))];
|
|
129
139
|
const lines = source.split(/\r?\n/);
|
|
130
|
-
return problems.filter((problem) => problem.
|
|
140
|
+
return problems.filter((problem) => options.rules?.[problem.code] !== "off").map((problem) => ({
|
|
141
|
+
...problem,
|
|
142
|
+
severity: options.rules?.[problem.code] === "error" ? "error" : options.rules?.[problem.code] === "warning" ? "warning" : problem.severity
|
|
143
|
+
})).filter((problem) => problem.line === null || !isIgnored(lines, problem.line, problem.code)).sort((first, second) => (first.line ?? 0) - (second.line ?? 0) || (first.column ?? 0) - (second.column ?? 0));
|
|
131
144
|
}
|
|
132
145
|
/** `WL9004` when the project tsconfig turns on legacy decorators. */
|
|
133
146
|
function checkTsconfigConventions(cwd, project = "tsconfig.json") {
|
|
@@ -155,7 +168,14 @@ function checkTsconfigConventions(cwd, project = "tsconfig.json") {
|
|
|
155
168
|
* project tsconfig.
|
|
156
169
|
*/
|
|
157
170
|
function runConventions(cwd, files, project) {
|
|
158
|
-
const
|
|
171
|
+
const options = readConventionOptions(cwd);
|
|
172
|
+
const problems = checkTsconfigConventions(cwd, project).filter((problem) => options.rules?.[problem.code] !== "off").map((problem) => {
|
|
173
|
+
const severity = options.rules?.[problem.code];
|
|
174
|
+
return severity === "error" || severity === "warning" ? {
|
|
175
|
+
...problem,
|
|
176
|
+
severity
|
|
177
|
+
} : problem;
|
|
178
|
+
});
|
|
159
179
|
for (const file of files) {
|
|
160
180
|
let source;
|
|
161
181
|
try {
|
|
@@ -163,11 +183,33 @@ function runConventions(cwd, files, project) {
|
|
|
163
183
|
} catch {
|
|
164
184
|
continue;
|
|
165
185
|
}
|
|
166
|
-
problems.push(...checkFileConventions(file, source, cwd));
|
|
186
|
+
problems.push(...checkFileConventions(file, source, cwd, options));
|
|
167
187
|
}
|
|
168
188
|
return problems;
|
|
169
189
|
}
|
|
190
|
+
/** Optional JSON configuration; malformed configuration is reported by the check step. */
|
|
191
|
+
function readConventionOptions(cwd) {
|
|
192
|
+
const file = path.join(cwd, ".warlock-check.json");
|
|
193
|
+
if (!existsSync(file)) return {};
|
|
194
|
+
const value = JSON.parse(readFileSync(file, "utf8"));
|
|
195
|
+
if (!value || typeof value !== "object") throw new Error(".warlock-check.json must be an object.");
|
|
196
|
+
const limit = Reflect.get(value, "componentLineLimit");
|
|
197
|
+
if (limit !== void 0 && (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1)) throw new Error("componentLineLimit must be a positive integer.");
|
|
198
|
+
const rules = Reflect.get(value, "rules");
|
|
199
|
+
const overrides = {};
|
|
200
|
+
if (rules !== void 0) {
|
|
201
|
+
if (!rules || typeof rules !== "object" || Array.isArray(rules)) throw new Error("rules must be an object.");
|
|
202
|
+
for (const [code, severity] of Object.entries(rules)) {
|
|
203
|
+
if (!isWarlockErrorCode(code) || severity !== "off" && severity !== "warning" && severity !== "error") throw new Error("rules must map registered WL codes to off, warning or error.");
|
|
204
|
+
overrides[code] = severity;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
...typeof limit === "number" ? { componentLineLimit: limit } : {},
|
|
209
|
+
rules: overrides
|
|
210
|
+
};
|
|
211
|
+
}
|
|
170
212
|
|
|
171
213
|
//#endregion
|
|
172
|
-
export { checkFileConventions, checkTsconfigConventions, isAutoLoadedFile, runConventions };
|
|
214
|
+
export { checkFileConventions, checkTsconfigConventions, isAutoLoadedFile, readConventionOptions, resolveSpecifier, runConventions };
|
|
173
215
|
//# sourceMappingURL=conventions-step.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conventions-step.mjs","names":["ast.parseSourceFile"],"sources":["../../../../../../../../../packages/core/src/tooling/check/conventions-step.ts"],"sourcesContent":["import { 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 { codeStyleFindings } from \"./code-style-conventions\";\nimport { securityFindings } from \"./security-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. */\nfunction resolveSpecifier(specifier: string, importer: string, cwd: string): 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 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 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): ToolingProblem[] {\n const sourceFile = ast.parseSourceFile(relativePath, source);\n const problems = [\n ...importProblems(sourceFile, relativePath, cwd),\n ...[...securityFindings(sourceFile), ...codeStyleFindings(sourceFile)].map((finding) =>\n findingProblem(sourceFile, relativePath, finding),\n ),\n ];\n const lines = source.split(/\\r?\\n/);\n\n return problems\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 problems = checkTsconfigConventions(cwd, project);\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));\n }\n\n return problems;\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 { 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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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixes.d.mts","names":[],"sources":["../../../../../../../../../packages/core/src/tooling/check/fixes.ts"],"mappings":";YAKY;EAAe;EAAe;EAAa"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { importSpecifiers } from "../syntax/imports.mjs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
//#region ../../packages/core/src/tooling/check/fixes.ts
|
|
6
|
+
/** Only relative .js imports: removing the suffix preserves TypeScript resolution. */
|
|
7
|
+
function importFixes(file, source) {
|
|
8
|
+
return importSpecifiers(file, source).filter(({ specifier, node }) => specifier.startsWith(".") && specifier.endsWith(".js") && /\.js['"]$/.test(source.slice(node.start, node.end))).map(({ node }) => ({
|
|
9
|
+
start: node.end - 4,
|
|
10
|
+
end: node.end - 1,
|
|
11
|
+
text: ""
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
function applyConventionFixes(cwd, problems) {
|
|
15
|
+
const files = /* @__PURE__ */ new Map();
|
|
16
|
+
for (const problem of problems) if (problem.file && problem.fix) {
|
|
17
|
+
const fixes = files.get(problem.file) ?? [];
|
|
18
|
+
fixes.push(problem.fix);
|
|
19
|
+
files.set(problem.file, fixes);
|
|
20
|
+
}
|
|
21
|
+
for (const [file, fixes] of files) {
|
|
22
|
+
const target = path.join(cwd, file);
|
|
23
|
+
let source = readFileSync(target, "utf8");
|
|
24
|
+
for (const fix of [...fixes].sort((a, b) => b.start - a.start)) source = source.slice(0, fix.start) + fix.text + source.slice(fix.end);
|
|
25
|
+
writeFileSync(target, source);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
export { applyConventionFixes, importFixes };
|
|
31
|
+
//# sourceMappingURL=fixes.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixes.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/tooling/check/fixes.ts"],"sourcesContent":["import { readFileSync, writeFileSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { ToolingProblem } from \"../problem\";\nimport { importSpecifiers } from \"../syntax/imports\";\n\nexport type ProblemFix = { start: number; end: number; text: string };\n\n/** Only relative .js imports: removing the suffix preserves TypeScript resolution. */\nexport function importFixes(file: string, source: string): ProblemFix[] {\n return importSpecifiers(file, source)\n .filter(\n ({ specifier, node }) =>\n specifier.startsWith(\".\") &&\n specifier.endsWith(\".js\") &&\n /\\.js['\"]$/.test(source.slice(node.start, node.end)),\n )\n .map(({ node }) => ({ start: node.end - 4, end: node.end - 1, text: \"\" }));\n}\n\nexport function applyConventionFixes(cwd: string, problems: readonly ToolingProblem[]): void {\n const files = new Map<string, ProblemFix[]>();\n for (const problem of problems)\n if (problem.file && problem.fix) {\n const fixes = files.get(problem.file) ?? [];\n fixes.push(problem.fix);\n files.set(problem.file, fixes);\n }\n for (const [file, fixes] of files) {\n const target = path.join(cwd, file);\n let source = readFileSync(target, \"utf8\");\n for (const fix of [...fixes].sort((a, b) => b.start - a.start))\n source = source.slice(0, fix.start) + fix.text + source.slice(fix.end);\n writeFileSync(target, source);\n }\n}\n"],"mappings":";;;;;;AAQA,SAAgB,YAAY,MAAc,QAA8B;CACtE,OAAO,iBAAiB,MAAM,MAAM,CAAC,CAClC,QACE,EAAE,WAAW,WACZ,UAAU,WAAW,GAAG,KACxB,UAAU,SAAS,KAAK,KACxB,YAAY,KAAK,OAAO,MAAM,KAAK,OAAO,KAAK,GAAG,CAAC,CACvD,CAAC,CACA,KAAK,EAAE,YAAY;EAAE,OAAO,KAAK,MAAM;EAAG,KAAK,KAAK,MAAM;EAAG,MAAM;CAAG,EAAE;AAC7E;AAEA,SAAgB,qBAAqB,KAAa,UAA2C;CAC3F,MAAM,wBAAQ,IAAI,IAA0B;CAC5C,KAAK,MAAM,WAAW,UACpB,IAAI,QAAQ,QAAQ,QAAQ,KAAK;EAC/B,MAAM,QAAQ,MAAM,IAAI,QAAQ,IAAI,KAAK,CAAC;EAC1C,MAAM,KAAK,QAAQ,GAAG;EACtB,MAAM,IAAI,QAAQ,MAAM,KAAK;CAC/B;CACF,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO;EACjC,MAAM,SAAS,KAAK,KAAK,KAAK,IAAI;EAClC,IAAI,SAAS,aAAa,QAAQ,MAAM;EACxC,KAAK,MAAM,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK,GAC3D,SAAS,OAAO,MAAM,GAAG,IAAI,KAAK,IAAI,IAAI,OAAO,OAAO,MAAM,IAAI,GAAG;EACvE,cAAc,QAAQ,MAAM;CAC9B;AACF"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ProblemFix } from "./fixes.mjs";
|
|
2
|
+
import { GitResult, GitRunner, listChangedFiles, runGit } from "./changed-files.mjs";
|
|
3
|
+
import { CHECK_EXIT_CODES, CHECK_STEPS, CheckExitCode, CheckReport, CheckStep, CheckStepName, CheckStepStatus, checkExitCode } from "./check-report.types.mjs";
|
|
4
|
+
import { ConventionOptions } from "./web-conventions.mjs";
|
|
5
|
+
import { checkFileConventions, checkTsconfigConventions, isAutoLoadedFile, runConventions } from "./conventions-step.mjs";
|
|
6
|
+
import { MIN_NODE_VERSION, checkNodeVersion } from "./environment-step.mjs";
|
|
7
|
+
import { LintOutcome, runLint } from "./lint-step.mjs";
|
|
8
|
+
import { VitestRun, VitestRunner, findVitest, parseVitestReport, runVitest } from "./tests-step.mjs";
|
|
9
|
+
import { CheckOptions, TypecheckRunner, runCheck } from "./run-check.mjs";
|
|
10
|
+
import { RunTestsOptions, TestRunReport, runTests, testRunArguments } from "./run-tests.mjs";
|
|
11
|
+
import { affectedFiles } from "./affected-files.mjs";
|
|
12
|
+
export { CHECK_EXIT_CODES, CHECK_STEPS, type CheckExitCode, type CheckOptions, type CheckReport, type CheckStep, type CheckStepName, type CheckStepStatus, type ConventionOptions, type GitResult, type GitRunner, type LintOutcome, MIN_NODE_VERSION, type ProblemFix, type RunTestsOptions, type TestRunReport, type TypecheckRunner, type VitestRun, type VitestRunner, affectedFiles, checkExitCode, checkFileConventions, checkNodeVersion, checkTsconfigConventions, findVitest, isAutoLoadedFile, listChangedFiles, parseVitestReport, runCheck, runConventions, runGit, runLint, runTests, runVitest, testRunArguments };
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { CHECK_EXIT_CODES, CHECK_STEPS, checkExitCode } from "./check-report.types.mjs";
|
|
2
|
-
import { listChangedFiles, runGit } from "./changed-files.mjs";
|
|
3
2
|
import { checkFileConventions, checkTsconfigConventions, isAutoLoadedFile, runConventions } from "./conventions-step.mjs";
|
|
3
|
+
import { affectedFiles } from "./affected-files.mjs";
|
|
4
|
+
import { listChangedFiles, runGit } from "./changed-files.mjs";
|
|
4
5
|
import { MIN_NODE_VERSION, checkNodeVersion } from "./environment-step.mjs";
|
|
5
6
|
import { runLint } from "./lint-step.mjs";
|
|
6
7
|
import { findVitest, parseVitestReport, runVitest } from "./tests-step.mjs";
|
|
7
8
|
import { runCheck } from "./run-check.mjs";
|
|
8
9
|
import { runTests, testRunArguments } from "./run-tests.mjs";
|
|
9
10
|
|
|
10
|
-
export { CHECK_EXIT_CODES, CHECK_STEPS, MIN_NODE_VERSION, checkExitCode, checkFileConventions, checkNodeVersion, checkTsconfigConventions, findVitest, isAutoLoadedFile, listChangedFiles, parseVitestReport, runCheck, runConventions, runGit, runLint, runTests, runVitest, testRunArguments };
|
|
11
|
+
export { CHECK_EXIT_CODES, CHECK_STEPS, MIN_NODE_VERSION, affectedFiles, checkExitCode, checkFileConventions, checkNodeVersion, checkTsconfigConventions, findVitest, isAutoLoadedFile, listChangedFiles, parseVitestReport, runCheck, runConventions, runGit, runLint, runTests, runVitest, testRunArguments };
|
|
@@ -13,6 +13,8 @@ export type CheckOptions = {
|
|
|
13
13
|
lint?: "oxlint";
|
|
14
14
|
/** Limit every step to the files changed against `base` (default HEAD) plus untracked files. */
|
|
15
15
|
changed?: boolean;
|
|
16
|
+
/** Apply safe convention fixes before checking. */
|
|
17
|
+
fix?: boolean;
|
|
16
18
|
/** Compare with `git merge-base <base> HEAD` instead of HEAD. Implies `changed`. */
|
|
17
19
|
base?: string;
|
|
18
20
|
/** Steps not to run. */
|
|
@@ -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":";;;;;;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,11 +1,13 @@
|
|
|
1
1
|
import { CHECK_STEPS, checkExitCode } from "./check-report.types.mjs";
|
|
2
2
|
import { createProblem } from "../problem.mjs";
|
|
3
|
+
import { applyConventionFixes } from "./fixes.mjs";
|
|
4
|
+
import { runConventions } from "./conventions-step.mjs";
|
|
5
|
+
import { listSourceFiles, listTestFiles } from "./source-files.mjs";
|
|
6
|
+
import { affectedFiles } from "./affected-files.mjs";
|
|
3
7
|
import { runTypecheck } from "../typecheck/run-typecheck.mjs";
|
|
4
8
|
import { listChangedFiles } from "./changed-files.mjs";
|
|
5
|
-
import { runConventions } from "./conventions-step.mjs";
|
|
6
9
|
import { checkNodeVersion } from "./environment-step.mjs";
|
|
7
10
|
import { runLint } from "./lint-step.mjs";
|
|
8
|
-
import { listSourceFiles, listTestFiles } from "./source-files.mjs";
|
|
9
11
|
import { findVitest, parseVitestReport, runVitest, vitestArguments, vitestRunFailure } from "./tests-step.mjs";
|
|
10
12
|
import path from "node:path";
|
|
11
13
|
|
|
@@ -85,12 +87,17 @@ async function runCheck(options = {}) {
|
|
|
85
87
|
const listed = await listChangedFiles({
|
|
86
88
|
cwd,
|
|
87
89
|
base: options.base,
|
|
88
|
-
git: options.git
|
|
90
|
+
git: options.git,
|
|
91
|
+
includeDeleted: true
|
|
89
92
|
});
|
|
90
93
|
if ("problem" in listed) {
|
|
91
94
|
filesBlocked = true;
|
|
92
95
|
found.push(listed.problem);
|
|
93
|
-
} else
|
|
96
|
+
} else {
|
|
97
|
+
filesBlocked = true;
|
|
98
|
+
files = await affectedFiles(cwd, listed.files, options.project);
|
|
99
|
+
filesBlocked = false;
|
|
100
|
+
}
|
|
94
101
|
}
|
|
95
102
|
return {
|
|
96
103
|
detail: `Node ${options.nodeVersion ?? process.versions.node}${files ? `, ${files.length} changed file(s)` : ""}`,
|
|
@@ -98,6 +105,15 @@ async function runCheck(options = {}) {
|
|
|
98
105
|
};
|
|
99
106
|
});
|
|
100
107
|
const targets = async () => files ?? await listSourceFiles(cwd);
|
|
108
|
+
if (options.fix && !filesBlocked && !skip.has("conventions")) try {
|
|
109
|
+
applyConventionFixes(cwd, runConventions(cwd, await targets(), options.project));
|
|
110
|
+
} catch (error) {
|
|
111
|
+
problems.push(createProblem({
|
|
112
|
+
code: "WL8007",
|
|
113
|
+
source: "environment",
|
|
114
|
+
message: `Could not apply fixes: ${errorMessage(error)}`
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
101
117
|
await run("typecheck", async () => {
|
|
102
118
|
if (files !== null && files.length === 0) return {
|
|
103
119
|
status: "skipped",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-check.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/tooling/check/run-check.ts"],"sourcesContent":["import 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 /** 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({ cwd, base: options.base, git: options.git });\n\n if (\"problem\" in listed) {\n filesBlocked = true;\n found.push(listed.problem);\n } else {\n files = listed.files;\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 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 { 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,7 +1,7 @@
|
|
|
1
1
|
import { checkExitCode } from "./check-report.types.mjs";
|
|
2
2
|
import { createProblem } from "../problem.mjs";
|
|
3
|
-
import { listChangedFiles } from "./changed-files.mjs";
|
|
4
3
|
import { listTestFiles } from "./source-files.mjs";
|
|
4
|
+
import { listChangedFiles } from "./changed-files.mjs";
|
|
5
5
|
import { findVitest, parseVitestReport, runVitest, vitestArguments, vitestRunFailure } from "./tests-step.mjs";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "../syntax/ast.mjs";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { WarlockErrorCode } from "../../errors/error-codes.mjs";
|
|
2
|
+
import "../syntax/ast.mjs";
|
|
3
|
+
import "./source-ast.mjs";
|
|
4
|
+
//#region ../../packages/core/src/tooling/check/web-conventions.d.ts
|
|
5
|
+
export type ConventionOptions = {
|
|
6
|
+
componentLineLimit?: number;
|
|
7
|
+
rules?: Partial<Record<WarlockErrorCode, "off" | "warning" | "error">>;
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
10
|
+
//# sourceMappingURL=web-conventions.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-conventions.d.mts","names":["_$______errors_error_codes0.WarlockErrorCode"],"sources":["../../../../../../../../../packages/core/src/tooling/check/web-conventions.ts"],"mappings":";;;;YAaY;EACV;EACA,QAAQ,QACN,OAHQA"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { parentOf, textOf } from "../syntax/ast.mjs";
|
|
2
|
+
import { ancestors, calleeName, isFunctionLike, keyName, literalString, memberName, unwrap, walk } from "./source-ast.mjs";
|
|
3
|
+
|
|
4
|
+
//#region ../../packages/core/src/tooling/check/web-conventions.ts
|
|
5
|
+
/** JSX-returning functions are components; helpers and hook files are not. */
|
|
6
|
+
function webFindings(source, options = {}) {
|
|
7
|
+
if (!/^src\/web\/.*\.tsx$/.test(source.fileName)) return [];
|
|
8
|
+
const findings = [];
|
|
9
|
+
const hooks = /* @__PURE__ */ new Set([
|
|
10
|
+
"useState",
|
|
11
|
+
"useReducer",
|
|
12
|
+
"useEffect",
|
|
13
|
+
"useLayoutEffect"
|
|
14
|
+
]);
|
|
15
|
+
for (const statement of source.body) if (statement.type === "ImportDeclaration" && statement.source.value === "react") {
|
|
16
|
+
for (const specifier of statement.specifiers) if (specifier.type === "ImportSpecifier" && hooks.has(keyName(specifier.imported) ?? "")) hooks.add(specifier.local.name);
|
|
17
|
+
}
|
|
18
|
+
const components = [];
|
|
19
|
+
walk(source, (node) => {
|
|
20
|
+
if (!isFunctionLike(node) || !node.body || ancestors(node).some(isFunctionLike)) return;
|
|
21
|
+
const parent = parentOf(node);
|
|
22
|
+
const name = node.id?.name ?? (parent?.type === "VariableDeclarator" ? keyName(parent.id) : void 0);
|
|
23
|
+
if (name && !/^[A-Z]/.test(name)) return;
|
|
24
|
+
let jsx = false;
|
|
25
|
+
walk(node.body, (child) => {
|
|
26
|
+
if (child.type === "JSXElement" || child.type === "JSXFragment") jsx = true;
|
|
27
|
+
});
|
|
28
|
+
if (jsx) components.push(node);
|
|
29
|
+
}, true);
|
|
30
|
+
const add = (code, node, message) => findings.push({
|
|
31
|
+
code,
|
|
32
|
+
node,
|
|
33
|
+
message,
|
|
34
|
+
severity: "warning"
|
|
35
|
+
});
|
|
36
|
+
const limit = options.componentLineLimit ?? 150;
|
|
37
|
+
if (components.length && source.text.replace(/\r?\n$/, "").split(/\r?\n/).length > limit) add("WL9013", components[0] ?? source, `Component file exceeds ${limit} lines. Split it into smaller components and use-*.ts hooks.`);
|
|
38
|
+
for (const component of components.slice(1)) add("WL9014", component, "More than one component in this file. Move each component to its own file.");
|
|
39
|
+
for (const component of components) {
|
|
40
|
+
if (!isFunctionLike(component) || !component.body) continue;
|
|
41
|
+
const eventNames = /* @__PURE__ */ new Set();
|
|
42
|
+
walk(component.body, (node) => {
|
|
43
|
+
if (node.type === "JSXAttribute" && /^on[A-Z]/.test(keyName(node.name) ?? "") && node.value?.type === "JSXExpressionContainer" && node.value.expression.type === "Identifier") eventNames.add(node.value.expression.name);
|
|
44
|
+
});
|
|
45
|
+
walk(component.body, (node) => {
|
|
46
|
+
if (node.type === "CallExpression" && hooks.has(calleeName(node) ?? "")) add("WL9015", node, "Move component state and effects to a use-*.ts hook file.");
|
|
47
|
+
if (isFunctionLike(node)) {
|
|
48
|
+
const parent = parentOf(node);
|
|
49
|
+
const name = node.id?.name ?? (parent?.type === "VariableDeclarator" ? keyName(parent.id) : void 0);
|
|
50
|
+
const attribute = parent?.type === "JSXExpressionContainer" ? parentOf(parent) : void 0;
|
|
51
|
+
if (/^(?:handle|on)[A-Z]/.test(name ?? "") || eventNames.has(name ?? "") || attribute?.type === "JSXAttribute" && /^on[A-Z]/.test(keyName(attribute.name) ?? "")) add("WL9015", node, "Move this event-handler function to a use-*.ts hook file.");
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
walk(source, (node) => {
|
|
56
|
+
if (node.type !== "JSXOpeningElement" || node.name.type !== "JSXIdentifier") return;
|
|
57
|
+
const tag = node.name.name;
|
|
58
|
+
if (![
|
|
59
|
+
"input",
|
|
60
|
+
"textarea",
|
|
61
|
+
"select"
|
|
62
|
+
].includes(tag)) return;
|
|
63
|
+
const attributes = node.attributes.filter((attribute) => attribute.type === "JSXAttribute");
|
|
64
|
+
const value = (attribute) => attribute.value?.type === "JSXExpressionContainer" ? literalString(attribute.value.expression) : literalString(attribute.value);
|
|
65
|
+
const inputType = attributes.find((attribute) => keyName(attribute.name) === "type");
|
|
66
|
+
const type = inputType ? value(inputType) : void 0;
|
|
67
|
+
if (!source.fileName.startsWith("src/web/design-system/") && (tag === "select" || tag === "input" && [
|
|
68
|
+
"checkbox",
|
|
69
|
+
"radio",
|
|
70
|
+
"date",
|
|
71
|
+
"time",
|
|
72
|
+
"datetime-local"
|
|
73
|
+
].includes(type ?? ""))) add("WL9016", node, "Use a design-system control instead of this native form control.");
|
|
74
|
+
for (const attribute of attributes) {
|
|
75
|
+
const name = keyName(attribute.name);
|
|
76
|
+
if ([
|
|
77
|
+
"required",
|
|
78
|
+
"pattern",
|
|
79
|
+
"min",
|
|
80
|
+
"max",
|
|
81
|
+
"minLength",
|
|
82
|
+
"maxLength"
|
|
83
|
+
].includes(name ?? "") || tag === "input" && name === "type" && type === "email") add("WL9017", attribute, "Move native validation to the shared validation schema and display field errors through the design-system control.");
|
|
84
|
+
}
|
|
85
|
+
}, true);
|
|
86
|
+
return findings;
|
|
87
|
+
}
|
|
88
|
+
/** RequestHandler exports declare their contract through a static responseSchema. */
|
|
89
|
+
function responseSchemaFindings(source) {
|
|
90
|
+
if (!/^src\/app\/.+\/controllers\/.+\.[cm]?tsx?$/.test(source.fileName)) return [];
|
|
91
|
+
const handlerTypes = /* @__PURE__ */ new Set(["RequestHandler"]);
|
|
92
|
+
for (const statement of source.body) if (statement.type === "ImportDeclaration") {
|
|
93
|
+
for (const specifier of statement.specifiers) if (specifier.type === "ImportSpecifier" && keyName(specifier.imported) === "RequestHandler") handlerTypes.add(specifier.local.name);
|
|
94
|
+
}
|
|
95
|
+
const exports = /* @__PURE__ */ new Set();
|
|
96
|
+
const schemas = /* @__PURE__ */ new Set();
|
|
97
|
+
const handlers = [];
|
|
98
|
+
walk(source, (node) => {
|
|
99
|
+
if (node.type === "ExportNamedDeclaration") for (const specifier of node.specifiers) exports.add(keyName(specifier.local) ?? "");
|
|
100
|
+
if (node.type === "ExportDefaultDeclaration" && node.declaration.type === "Identifier") exports.add(node.declaration.name);
|
|
101
|
+
if (ancestors(node).some(isFunctionLike)) return;
|
|
102
|
+
if (node.type === "AssignmentExpression" && node.operator === "=") {
|
|
103
|
+
const left = unwrap(node.left);
|
|
104
|
+
if (left.type === "MemberExpression" && memberName(left) === "responseSchema" && left.object.type === "Identifier" && !["undefined", "null"].includes(textOf(node.right))) schemas.add(left.object.name);
|
|
105
|
+
}
|
|
106
|
+
if (node.type !== "VariableDeclarator" || node.id.type !== "Identifier") return;
|
|
107
|
+
const annotation = node.id.typeAnnotation?.typeAnnotation;
|
|
108
|
+
const initializer = node.init;
|
|
109
|
+
const satisfied = initializer?.type === "TSSatisfiesExpression" ? initializer.typeAnnotation : void 0;
|
|
110
|
+
const type = annotation ?? satisfied;
|
|
111
|
+
if (type?.type !== "TSTypeReference" || !handlerTypes.has(keyName(type.typeName) ?? "")) return;
|
|
112
|
+
handlers.push(node);
|
|
113
|
+
if (initializer?.type === "CallExpression" && initializer.callee.type === "MemberExpression" && initializer.callee.object.type === "Identifier" && initializer.callee.object.name === "Object" && keyName(initializer.callee.property) === "assign" && initializer.arguments.slice(1).some((argument) => argument.type === "ObjectExpression" && argument.properties.some((property) => property.type === "Property" && keyName(property.key) === "responseSchema"))) schemas.add(node.id.name);
|
|
114
|
+
if (ancestors(node).some((parent) => parent.type === "ExportNamedDeclaration")) exports.add(node.id.name);
|
|
115
|
+
}, true);
|
|
116
|
+
return handlers.filter((node) => node.id.type === "Identifier" && exports.has(node.id.name) && !schemas.has(node.id.name)).map((node) => ({
|
|
117
|
+
code: "WL9018",
|
|
118
|
+
node: node.id,
|
|
119
|
+
severity: "warning",
|
|
120
|
+
message: "Exported RequestHandler has no responseSchema. Declare the response body contract for each returned status code."
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
//#endregion
|
|
125
|
+
export { responseSchemaFindings, webFindings };
|
|
126
|
+
//# sourceMappingURL=web-conventions.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web-conventions.mjs","names":["ast.parentOf","ast.textOf"],"sources":["../../../../../../../../../packages/core/src/tooling/check/web-conventions.ts"],"sourcesContent":["import * as ast from \"../syntax/ast\";\nimport {\n ancestors,\n calleeName,\n isFunctionLike,\n keyName,\n literalString,\n memberName,\n unwrap,\n walk,\n type SourceFinding,\n} from \"./source-ast\";\n\nexport type ConventionOptions = {\n componentLineLimit?: number;\n rules?: Partial<\n Record<import(\"../../errors/error-codes\").WarlockErrorCode, \"off\" | \"warning\" | \"error\">\n >;\n};\n\n/** JSX-returning functions are components; helpers and hook files are not. */\nexport function webFindings(\n source: ast.SourceFile,\n options: ConventionOptions = {},\n): SourceFinding[] {\n if (!/^src\\/web\\/.*\\.tsx$/.test(source.fileName)) return [];\n const findings: SourceFinding[] = [];\n const hooks = new Set([\"useState\", \"useReducer\", \"useEffect\", \"useLayoutEffect\"]);\n for (const statement of source.body)\n if (statement.type === \"ImportDeclaration\" && statement.source.value === \"react\") {\n for (const specifier of statement.specifiers)\n if (specifier.type === \"ImportSpecifier\" && hooks.has(keyName(specifier.imported) ?? \"\"))\n hooks.add(specifier.local.name);\n }\n const components: ast.Node[] = [];\n walk(\n source,\n (node) => {\n if (!isFunctionLike(node) || !node.body || ancestors(node).some(isFunctionLike)) return;\n const parent = ast.parentOf(node);\n const name =\n node.id?.name ?? (parent?.type === \"VariableDeclarator\" ? keyName(parent.id) : undefined);\n if (name && !/^[A-Z]/.test(name)) return;\n let jsx = false;\n walk(node.body, (child) => {\n if (child.type === \"JSXElement\" || child.type === \"JSXFragment\") jsx = true;\n });\n if (jsx) components.push(node);\n },\n true,\n );\n const add = (code: SourceFinding[\"code\"], node: ast.Node, message: string) =>\n findings.push({ code, node, message, severity: \"warning\" });\n const limit = options.componentLineLimit ?? 150;\n if (components.length && source.text.replace(/\\r?\\n$/, \"\").split(/\\r?\\n/).length > limit)\n add(\n \"WL9013\",\n components[0] ?? source,\n `Component file exceeds ${limit} lines. Split it into smaller components and use-*.ts hooks.`,\n );\n for (const component of components.slice(1))\n add(\n \"WL9014\",\n component,\n \"More than one component in this file. Move each component to its own file.\",\n );\n for (const component of components) {\n if (!isFunctionLike(component) || !component.body) continue;\n const eventNames = new Set<string>();\n walk(component.body, (node) => {\n if (\n node.type === \"JSXAttribute\" &&\n /^on[A-Z]/.test(keyName(node.name) ?? \"\") &&\n node.value?.type === \"JSXExpressionContainer\" &&\n node.value.expression.type === \"Identifier\"\n )\n eventNames.add(node.value.expression.name);\n });\n walk(component.body, (node) => {\n if (node.type === \"CallExpression\" && hooks.has(calleeName(node) ?? \"\"))\n add(\"WL9015\", node, \"Move component state and effects to a use-*.ts hook file.\");\n if (isFunctionLike(node)) {\n const parent = ast.parentOf(node);\n const name =\n node.id?.name ?? (parent?.type === \"VariableDeclarator\" ? keyName(parent.id) : undefined);\n const attribute =\n parent?.type === \"JSXExpressionContainer\" ? ast.parentOf(parent) : undefined;\n if (\n /^(?:handle|on)[A-Z]/.test(name ?? \"\") ||\n eventNames.has(name ?? \"\") ||\n (attribute?.type === \"JSXAttribute\" && /^on[A-Z]/.test(keyName(attribute.name) ?? \"\"))\n )\n add(\"WL9015\", node, \"Move this event-handler function to a use-*.ts hook file.\");\n }\n });\n }\n walk(\n source,\n (node) => {\n if (node.type !== \"JSXOpeningElement\" || node.name.type !== \"JSXIdentifier\") return;\n const tag = node.name.name;\n if (![\"input\", \"textarea\", \"select\"].includes(tag)) return;\n const attributes = node.attributes.filter((attribute) => attribute.type === \"JSXAttribute\");\n const value = (attribute: ast.JSXAttribute) =>\n attribute.value?.type === \"JSXExpressionContainer\"\n ? literalString(attribute.value.expression)\n : literalString(attribute.value);\n const inputType = attributes.find((attribute) => keyName(attribute.name) === \"type\");\n const type = inputType ? value(inputType) : undefined;\n if (\n !source.fileName.startsWith(\"src/web/design-system/\") &&\n (tag === \"select\" ||\n (tag === \"input\" &&\n [\"checkbox\", \"radio\", \"date\", \"time\", \"datetime-local\"].includes(type ?? \"\")))\n )\n add(\"WL9016\", node, \"Use a design-system control instead of this native form control.\");\n for (const attribute of attributes) {\n const name = keyName(attribute.name);\n if (\n [\"required\", \"pattern\", \"min\", \"max\", \"minLength\", \"maxLength\"].includes(name ?? \"\") ||\n (tag === \"input\" && name === \"type\" && type === \"email\")\n )\n add(\n \"WL9017\",\n attribute,\n \"Move native validation to the shared validation schema and display field errors through the design-system control.\",\n );\n }\n },\n true,\n );\n return findings;\n}\n\n/** RequestHandler exports declare their contract through a static responseSchema. */\nexport function responseSchemaFindings(source: ast.SourceFile): SourceFinding[] {\n if (!/^src\\/app\\/.+\\/controllers\\/.+\\.[cm]?tsx?$/.test(source.fileName)) return [];\n const handlerTypes = new Set([\"RequestHandler\"]);\n for (const statement of source.body)\n if (statement.type === \"ImportDeclaration\")\n for (const specifier of statement.specifiers)\n if (\n specifier.type === \"ImportSpecifier\" &&\n keyName(specifier.imported) === \"RequestHandler\"\n )\n handlerTypes.add(specifier.local.name);\n const exports = new Set<string>();\n const schemas = new Set<string>();\n const handlers: ast.VariableDeclarator[] = [];\n walk(\n source,\n (node) => {\n if (node.type === \"ExportNamedDeclaration\")\n for (const specifier of node.specifiers) exports.add(keyName(specifier.local) ?? \"\");\n if (node.type === \"ExportDefaultDeclaration\" && node.declaration.type === \"Identifier\")\n exports.add(node.declaration.name);\n if (ancestors(node).some(isFunctionLike)) return;\n if (node.type === \"AssignmentExpression\" && node.operator === \"=\") {\n const left = unwrap(node.left);\n if (\n left.type === \"MemberExpression\" &&\n memberName(left) === \"responseSchema\" &&\n left.object.type === \"Identifier\" &&\n ![\"undefined\", \"null\"].includes(ast.textOf(node.right))\n )\n schemas.add(left.object.name);\n }\n if (node.type !== \"VariableDeclarator\" || node.id.type !== \"Identifier\") return;\n const annotation = node.id.typeAnnotation?.typeAnnotation;\n const initializer = node.init;\n const satisfied =\n initializer?.type === \"TSSatisfiesExpression\" ? initializer.typeAnnotation : undefined;\n const type = annotation ?? satisfied;\n if (type?.type !== \"TSTypeReference\" || !handlerTypes.has(keyName(type.typeName) ?? \"\"))\n return;\n handlers.push(node);\n if (\n initializer?.type === \"CallExpression\" &&\n initializer.callee.type === \"MemberExpression\" &&\n initializer.callee.object.type === \"Identifier\" &&\n initializer.callee.object.name === \"Object\" &&\n keyName(initializer.callee.property) === \"assign\" &&\n initializer.arguments\n .slice(1)\n .some(\n (argument) =>\n argument.type === \"ObjectExpression\" &&\n argument.properties.some(\n (property) =>\n property.type === \"Property\" && keyName(property.key) === \"responseSchema\",\n ),\n )\n )\n schemas.add(node.id.name);\n if (ancestors(node).some((parent) => parent.type === \"ExportNamedDeclaration\"))\n exports.add(node.id.name);\n },\n true,\n );\n return handlers\n .filter(\n (node) =>\n node.id.type === \"Identifier\" && exports.has(node.id.name) && !schemas.has(node.id.name),\n )\n .map<SourceFinding>((node) => ({\n code: \"WL9018\",\n node: node.id,\n severity: \"warning\",\n message:\n \"Exported RequestHandler has no responseSchema. Declare the response body contract for each returned status code.\",\n }));\n}\n"],"mappings":"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"}
|