@warlock.js/core 5.35.0 → 5.37.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 +53 -0
- package/esm/cli/commands/migrate.command.mjs +10 -0
- package/esm/cli/commands/migrate.command.mjs.map +1 -1
- package/esm/cli/commands/release/release-command.action.mjs +245 -0
- package/esm/cli/commands/release/release-command.action.mjs.map +1 -0
- package/esm/cli/commands/release.command.mjs +66 -0
- package/esm/cli/commands/release.command.mjs.map +1 -0
- package/esm/cli/framework-cli-commands.mjs +2 -0
- package/esm/cli/framework-cli-commands.mjs.map +1 -1
- package/esm/database/migrate-action.mjs +24 -2
- package/esm/database/migrate-action.mjs.map +1 -1
- package/esm/dev-server/loader/load-hook.d.mts.map +1 -1
- package/esm/dev-server/loader/load-hook.mjs +6 -0
- package/esm/dev-server/loader/load-hook.mjs.map +1 -1
- 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 +6 -3
- package/esm/index.mjs +4 -2
- 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 +12 -3
- package/esm/production/route-registration-snapshot.mjs.map +1 -1
- package/esm/release/activate.mjs +261 -0
- package/esm/release/activate.mjs.map +1 -0
- package/esm/release/candidate.mjs +107 -0
- package/esm/release/candidate.mjs.map +1 -0
- package/esm/release/cascade-migration-provider.mjs +32 -0
- package/esm/release/cascade-migration-provider.mjs.map +1 -0
- package/esm/release/config.mjs +60 -0
- package/esm/release/config.mjs.map +1 -0
- package/esm/release/gate.mjs +67 -0
- package/esm/release/gate.mjs.map +1 -0
- package/esm/release/migrations-proof.mjs +100 -0
- package/esm/release/migrations-proof.mjs.map +1 -0
- package/esm/release/prune.mjs +42 -0
- package/esm/release/prune.mjs.map +1 -0
- package/esm/release/report.mjs +25 -0
- package/esm/release/report.mjs.map +1 -0
- package/esm/release/runner.mjs +55 -0
- package/esm/release/runner.mjs.map +1 -0
- package/esm/release/semver.mjs +53 -0
- package/esm/release/semver.mjs.map +1 -0
- package/esm/release/stage.mjs +122 -0
- package/esm/release/stage.mjs.map +1 -0
- package/esm/release/store.mjs +131 -0
- package/esm/release/store.mjs.map +1 -0
- package/esm/release/types.d.mts +102 -0
- package/esm/release/types.d.mts.map +1 -0
- package/esm/release/verify.mjs +94 -0
- package/esm/release/verify.mjs.map +1 -0
- package/esm/router/index.d.mts +2 -2
- package/esm/router/index.mjs +2 -2
- package/esm/router/router.d.mts +4 -0
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +14 -3
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts +45 -0
- package/esm/router/types.d.mts.map +1 -1
- package/esm/router/unguarded-route-diagnostics.d.mts +6 -0
- package/esm/router/unguarded-route-diagnostics.d.mts.map +1 -1
- package/esm/router/unguarded-route-diagnostics.mjs +10 -2
- package/esm/router/unguarded-route-diagnostics.mjs.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/security-conventions.mjs +19 -1
- package/esm/tooling/check/security-conventions.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/tooling/inspect/collect-routes.mjs +3 -2
- package/esm/tooling/inspect/collect-routes.mjs.map +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/validation/init.mjs +11 -4
- package/esm/validation/init.mjs.map +1 -1
- package/esm/validation/translate-rule.mjs +63 -0
- package/esm/validation/translate-rule.mjs.map +1 -0
- package/esm/warlock-config/index.d.mts +3 -2
- package/esm/warlock-config/types.d.mts +8 -0
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/llms-full.txt +221 -18
- package/llms.txt +1 -0
- package/package.json +13 -13
- package/skills/deploy-release/SKILL.md +76 -0
- package/skills/index.md +1 -1
- package/skills/register-route/SKILL.md +25 -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
- package/skills/warlock-conventions/SKILL.md +1 -1
package/esm/router/types.d.mts
CHANGED
|
@@ -323,6 +323,31 @@ export interface RouteOptions {
|
|
|
323
323
|
* router.post("/auth/login", loginController, { public: true });
|
|
324
324
|
*/
|
|
325
325
|
public?: boolean;
|
|
326
|
+
/**
|
|
327
|
+
* Whether this named API route may be embedded in a page for browser use.
|
|
328
|
+
*
|
|
329
|
+
* Defaults to `true`. Set `false` for an endpoint that must never be
|
|
330
|
+
* reachable through `apiHref()` or `useSubmitForm({ route })` from client
|
|
331
|
+
* code (an internal webhook, an admin-only job trigger). Such a route is never
|
|
332
|
+
* written into any page's browser route table, and a client reference to its
|
|
333
|
+
* name is a `warlock check` error (`WL9020`). Server-side authorization still
|
|
334
|
+
* decides who may call the endpoint.
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* router.post("/internal/reindex", reindexController, { name: "internal.reindex", clientVisible: false });
|
|
338
|
+
*/
|
|
339
|
+
clientVisible?: boolean;
|
|
340
|
+
/**
|
|
341
|
+
* The sites (`web.sites` keys) a multi-site app offers this named API route
|
|
342
|
+
* to in the browser. Absent means the route is shared: every site may embed
|
|
343
|
+
* it. A route registered inside a `router.group({ sites })` inherits the
|
|
344
|
+
* group's list unless it declares its own. This scopes browser route tables
|
|
345
|
+
* only; the endpoint itself still answers on every host.
|
|
346
|
+
*
|
|
347
|
+
* @example
|
|
348
|
+
* router.post("/api/admin/users", createUser, { name: "admin.users.create", sites: ["admin"] });
|
|
349
|
+
*/
|
|
350
|
+
sites?: readonly string[];
|
|
326
351
|
}
|
|
327
352
|
export type RequestMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "OPTIONS" | "HEAD" | "all";
|
|
328
353
|
/** Browser-safe snapshot metadata for a named application API route. */
|
|
@@ -337,6 +362,19 @@ export type NamedApiRoute = Readonly<{
|
|
|
337
362
|
* (paths relative to `.warlock/typings/`), object literal types and arrays.
|
|
338
363
|
*/
|
|
339
364
|
response?: Readonly<Record<string, string>>;
|
|
365
|
+
/**
|
|
366
|
+
* Present only as `false`, and only on a route that opted out with
|
|
367
|
+
* `clientVisible: false`. Absent means the route may be embedded in pages.
|
|
368
|
+
*/
|
|
369
|
+
clientVisible?: false;
|
|
370
|
+
/** The route's declared `sites`, present only when it declared some. */
|
|
371
|
+
sites?: readonly string[];
|
|
372
|
+
/**
|
|
373
|
+
* Present only as `true`: a required auth guard (see `markAuthGuard`) is in
|
|
374
|
+
* the route's middleware chain, group middleware included. Browser tables
|
|
375
|
+
* leave such a route out for guest viewers.
|
|
376
|
+
*/
|
|
377
|
+
guarded?: true;
|
|
340
378
|
}>;
|
|
341
379
|
/** Options for `router.getNamedApiRoutes()`. */
|
|
342
380
|
export type NamedApiRoutesOptions = Readonly<{
|
|
@@ -414,6 +452,11 @@ export type GroupedRoutesOptions = {
|
|
|
414
452
|
* opts it back out.
|
|
415
453
|
*/
|
|
416
454
|
public?: boolean;
|
|
455
|
+
/**
|
|
456
|
+
* Offer every named API route in the group only to these sites (see
|
|
457
|
+
* `RouteOptions.sites`). A route's own `sites` replaces it.
|
|
458
|
+
*/
|
|
459
|
+
sites?: readonly string[];
|
|
417
460
|
};
|
|
418
461
|
/** Route resource */
|
|
419
462
|
export type RouteResource = {
|
|
@@ -476,6 +519,8 @@ export type RouterStacks = {
|
|
|
476
519
|
* is open default to `public: true`.
|
|
477
520
|
*/
|
|
478
521
|
public?: number;
|
|
522
|
+
/** Open `router.group({ sites })` scopes, innermost last. */
|
|
523
|
+
sites?: (readonly string[])[];
|
|
479
524
|
/**
|
|
480
525
|
* Source file path for routes being added (for HMR route tracking)
|
|
481
526
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/router/types.ts"],"mappings":";;;;;;;;;;;;;YAUY,qBAAqB;;;;;;;YAQrB,WAAW,0BAA0B,UAAU;GACxD,SAAS,YAAY,qBAAqB;;YAGjC;YAEA,qBAAqB,uBAAuB;;;;YAK5C;YAEA,oBAAoB,gBAAgB;YAEpC,yBAAyB,iBAAiB,UAAU;;;;EAI9D,WAAW,WAAW;;;;;;EAMtB;;;;EAIA,SAAS;;;;;;;;EAQT,SAAS;;;;;;;;;;;;;;;;;;;EAmBT,YAAY,yBAAyB;;;;;YAM3B;;;;EAIV;;;;EAIA;;;;;YAMU,yBAAyB,iBAAiB,UAAU;EAC9D,SAAS;EACT,UAAU;;;;EAIV,OAAO;;;;;YAMG,yBAAyB,iBAAiB,UAAU,YAC9D,QAAQ,kBACR,SAAS,yBAAyB,cAEhC,+CAGA,QAAQ;iBAEK;;;;EAIf,SAAS;;;;EAIT,UAAU;;;;EAIV,WAAW,QAAQ;;;;EAInB;;;;EAIA,mBAAmB,QAAQ,sBAAsB;;;;;;;;;YAUvC,YAAY,iBAAiB,UAAU;;;;EAIjD,SAAS;;;;EAIT,UAAU;;YAGA,eAAe,iBAAiB,UAAU;;;;GAInD,SAAS,YAAY,YAAY;;;;EAKlC,aAAa,yBAAyB;;;;EAKtC;;;;;EAMA,iBAAiB;;;;;;YAOP;;;;EAIV;;;;EAIA;;;;EAIA;;;;;;YAOU;;;;;;;EAOV,YAAY;;iBAGG;;;;EAIf,aAAa;;;;EAIb;;;;EAIA;;;;;EAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2BA,gBAAgB;;;;;;EAMhB;;;;EAIA;;;;EAIA;;;;;;;;;;;;;;;;EAgBA,YAAY;;;;;;;;;;;;;;;;;;;;;;;EAuBZ;;;;;;;;;;;;;;EAcA,aAAa;;;;;;;;;;;EAWb,UAAU;;;;;;;;;;;;EAYV;;YAGU;;YAWA,gBAAgB;EAC1B;EACA;EACA,QAAQ;;;;;;;EAOR,WAAW,SAAS;;;
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/router/types.ts"],"mappings":";;;;;;;;;;;;;YAUY,qBAAqB;;;;;;;YAQrB,WAAW,0BAA0B,UAAU;GACxD,SAAS,YAAY,qBAAqB;;YAGjC;YAEA,qBAAqB,uBAAuB;;;;YAK5C;YAEA,oBAAoB,gBAAgB;YAEpC,yBAAyB,iBAAiB,UAAU;;;;EAI9D,WAAW,WAAW;;;;;;EAMtB;;;;EAIA,SAAS;;;;;;;;EAQT,SAAS;;;;;;;;;;;;;;;;;;;EAmBT,YAAY,yBAAyB;;;;;YAM3B;;;;EAIV;;;;EAIA;;;;;YAMU,yBAAyB,iBAAiB,UAAU;EAC9D,SAAS;EACT,UAAU;;;;EAIV,OAAO;;;;;YAMG,yBAAyB,iBAAiB,UAAU,YAC9D,QAAQ,kBACR,SAAS,yBAAyB,cAEhC,+CAGA,QAAQ;iBAEK;;;;EAIf,SAAS;;;;EAIT,UAAU;;;;EAIV,WAAW,QAAQ;;;;EAInB;;;;EAIA,mBAAmB,QAAQ,sBAAsB;;;;;;;;;YAUvC,YAAY,iBAAiB,UAAU;;;;EAIjD,SAAS;;;;EAIT,UAAU;;YAGA,eAAe,iBAAiB,UAAU;;;;GAInD,SAAS,YAAY,YAAY;;;;EAKlC,aAAa,yBAAyB;;;;EAKtC;;;;;EAMA,iBAAiB;;;;;;YAOP;;;;EAIV;;;;EAIA;;;;EAIA;;;;;;YAOU;;;;;;;EAOV,YAAY;;iBAGG;;;;EAIf,aAAa;;;;EAIb;;;;EAIA;;;;;EAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2BA,gBAAgB;;;;;;EAMhB;;;;EAIA;;;;EAIA;;;;;;;;;;;;;;;;EAgBA,YAAY;;;;;;;;;;;;;;;;;;;;;;;EAuBZ;;;;;;;;;;;;;;EAcA,aAAa;;;;;;;;;;;EAWb,UAAU;;;;;;;;;;;;EAYV;;;;;;;;;;;;;;EAcA;;;;;;;;;;;EAWA;;YAGU;;YAWA,gBAAgB;EAC1B;EACA;EACA,QAAQ;;;;;;;EAOR,WAAW,SAAS;;;;;EAKpB;;EAEA;;;;;;EAMA;;;YAIU,wBAAwB;;;;;EAKlC,mBACE,QAAQ,gBACR,OAAO,UACJ,SAAS;;;;;YAMJ,QAAQ;;;;EAIlB,QAAQ;;;;EAIR;;;;EAIA;;;;;EAKA,SAAS;;;;;;EAMT;;;;;;EAMA;;;;EAIA,YAAY;;;;;;EAMZ;;YAGU,YAAY,GAAG,gBAAgB,KAAK,KAAK,GAAG,KAAK,QAAQ,KAAK,GAAG;;;;YAKjE;;;;EAIV,aAAa;;;;EAIb;;;;;EAKA;;;;;;EAMA;;;;;EAKA;;;YAIU;;;;EAIV,OAAO;;;;EAIP,MAAM;;;;EAIN,SAAS;;;;EAIT,SAAS;;;;EAIT,QAAQ;;;;EAIR,SAAS;;;;EAIT,aAAa;;;;EAIb;;;;IAIE,MAAM;;;;IAIN,SAAS;;;;IAIT,SAAS;;;;IAIT,QAAQ;;;YAIA;EACV;EACA;EACA,YAAY;;;;;EAKZ;;EAEA;;;;EAIA"}
|
|
@@ -25,6 +25,12 @@ export declare function markAuthGuard<T extends (...args: never[]) => unknown>(m
|
|
|
25
25
|
* `guard`.
|
|
26
26
|
*/
|
|
27
27
|
export declare function isAuthGuardMiddleware(middleware: unknown): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Whether an auth guard is in the route's middleware chain. `route.middleware`
|
|
30
|
+
* already holds the middleware of every enclosing `router.group`, so a guard
|
|
31
|
+
* declared once on a group counts for each route inside it.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isRequiredGuardRoute(route: Pick<Route, "middleware">): boolean;
|
|
28
34
|
/** Whether a route mutates state, is not a page route, and is neither guarded nor public. */
|
|
29
35
|
export declare function isUnguardedMutatingRoute(route: Route): boolean;
|
|
30
36
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unguarded-route-diagnostics.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/router/unguarded-route-diagnostics.ts"],"mappings":";;;;;;;;qBAuBa;;;;;;;;;;;;wBAaG,cAAc,cAAc,2BAC1C,YAAY,GACZ,oBACC;;;;;;wBAea,sBAAsB;;
|
|
1
|
+
{"version":3,"file":"unguarded-route-diagnostics.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/router/unguarded-route-diagnostics.ts"],"mappings":";;;;;;;;qBAuBa;;;;;;;;;;;;wBAaG,cAAc,cAAc,2BAC1C,YAAY,GACZ,oBACC;;;;;;wBAea,sBAAsB;;;;;;wBActB,qBAAqB,OAAO,KAAK;;wBAKjC,yBAAyB,OAAO;;;;;wBAoBhC,8BACd,iBAAiB,SACjB,SAAQ,YAAY,eACnB;;wBAsCa"}
|
|
@@ -51,12 +51,20 @@ function isAuthGuardMiddleware(middleware) {
|
|
|
51
51
|
if (typeof marker === "boolean") return marker;
|
|
52
52
|
return isAuthGuardName(middleware.name);
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Whether an auth guard is in the route's middleware chain. `route.middleware`
|
|
56
|
+
* already holds the middleware of every enclosing `router.group`, so a guard
|
|
57
|
+
* declared once on a group counts for each route inside it.
|
|
58
|
+
*/
|
|
59
|
+
function isRequiredGuardRoute(route) {
|
|
60
|
+
return (route.middleware ?? []).some(isAuthGuardMiddleware);
|
|
61
|
+
}
|
|
54
62
|
/** Whether a route mutates state, is not a page route, and is neither guarded nor public. */
|
|
55
63
|
function isUnguardedMutatingRoute(route) {
|
|
56
64
|
if (!isMutatingHttpMethod(String(route.method))) return false;
|
|
57
65
|
if (route.isPage) return false;
|
|
58
66
|
if (route.public === true) return false;
|
|
59
|
-
return !(route
|
|
67
|
+
return !isRequiredGuardRoute(route);
|
|
60
68
|
}
|
|
61
69
|
/** Routes already reported in this process, so an HMR reload does not repeat them. */
|
|
62
70
|
const reported = /* @__PURE__ */ new Set();
|
|
@@ -91,5 +99,5 @@ function resetUnguardedRouteReports() {
|
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
//#endregion
|
|
94
|
-
export { AUTH_GUARD_MARKER, isAuthGuardMiddleware, isUnguardedMutatingRoute, markAuthGuard, reportUnguardedMutatingRoutes, resetUnguardedRouteReports };
|
|
102
|
+
export { AUTH_GUARD_MARKER, isAuthGuardMiddleware, isRequiredGuardRoute, isUnguardedMutatingRoute, markAuthGuard, reportUnguardedMutatingRoutes, resetUnguardedRouteReports };
|
|
95
103
|
//# sourceMappingURL=unguarded-route-diagnostics.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unguarded-route-diagnostics.mjs","names":[],"sources":["../../../../../../../../packages/core/src/router/unguarded-route-diagnostics.ts"],"sourcesContent":["/**\n * Development warning for mutating routes that nobody guarded.\n *\n * A POST/PUT/PATCH/DELETE route without auth middleware is reachable by anyone\n * who can reach the server. Sometimes that is the point (a login, a webhook, a\n * contact form) and sometimes it is a forgotten `guarded()` block. The router\n * cannot tell the two apart, so the app says which with `public: true`, and\n * `warlock dev` lists every mutating route that has neither.\n *\n * Warning only: routes register and behave exactly as before, nothing here\n * throws, and production never runs it. `warlock check` reports the same\n * routes from source as `WL9006`, before the app runs.\n */\n\nimport { log } from \"@warlock.js/logger\";\nimport { isAuthGuardName, isMutatingHttpMethod } from \"./route-guard-names\";\nimport type { Route } from \"./types\";\n\n/**\n * Marks a middleware as an authentication guard. A registry symbol, so a copy\n * of core loaded twice (a linked package next to an installed one) still\n * recognises guards the other copy marked.\n */\nexport const AUTH_GUARD_MARKER: unique symbol = Symbol.for(\"warlock.js.authGuard\");\n\n/**\n * Declare whether a middleware authenticates the request. `authMiddleware()`\n * from `@warlock.js/auth` marks itself (and marks `{ optional: true }` as not\n * a guard); use this for a hand-written guard whose name does not contain\n * `auth` or `guard`.\n *\n * @example\n * export const requireApiKey = markAuthGuard(async ({ request, response }) => {\n * if (request.headers[\"x-api-key\"] !== env(\"API_KEY\")) return response.unauthorized();\n * });\n */\nexport function markAuthGuard<T extends (...args: never[]) => unknown>(\n middleware: T,\n isGuard = true,\n): T {\n Object.defineProperty(middleware, AUTH_GUARD_MARKER, {\n value: isGuard,\n configurable: true,\n enumerable: false,\n });\n\n return middleware;\n}\n\n/**\n * Whether a middleware authenticates the request: its {@link markAuthGuard}\n * marker when it has one, otherwise a function name containing `auth` or\n * `guard`.\n */\nexport function isAuthGuardMiddleware(middleware: unknown): boolean {\n if (typeof middleware !== \"function\") return false;\n\n const marker: unknown = Reflect.get(middleware, AUTH_GUARD_MARKER);\n if (typeof marker === \"boolean\") return marker;\n\n return isAuthGuardName(middleware.name);\n}\n\n/** Whether a route mutates state, is not a page route, and is neither guarded nor public. */\nexport function isUnguardedMutatingRoute(route: Route): boolean {\n if (!isMutatingHttpMethod(String(route.method))) return false;\n // Page actions are authorized by the page itself (`requireUser()`).\n if (route.isPage) return false;\n if (route.public === true) return false;\n\n return !(route
|
|
1
|
+
{"version":3,"file":"unguarded-route-diagnostics.mjs","names":[],"sources":["../../../../../../../../packages/core/src/router/unguarded-route-diagnostics.ts"],"sourcesContent":["/**\n * Development warning for mutating routes that nobody guarded.\n *\n * A POST/PUT/PATCH/DELETE route without auth middleware is reachable by anyone\n * who can reach the server. Sometimes that is the point (a login, a webhook, a\n * contact form) and sometimes it is a forgotten `guarded()` block. The router\n * cannot tell the two apart, so the app says which with `public: true`, and\n * `warlock dev` lists every mutating route that has neither.\n *\n * Warning only: routes register and behave exactly as before, nothing here\n * throws, and production never runs it. `warlock check` reports the same\n * routes from source as `WL9006`, before the app runs.\n */\n\nimport { log } from \"@warlock.js/logger\";\nimport { isAuthGuardName, isMutatingHttpMethod } from \"./route-guard-names\";\nimport type { Route } from \"./types\";\n\n/**\n * Marks a middleware as an authentication guard. A registry symbol, so a copy\n * of core loaded twice (a linked package next to an installed one) still\n * recognises guards the other copy marked.\n */\nexport const AUTH_GUARD_MARKER: unique symbol = Symbol.for(\"warlock.js.authGuard\");\n\n/**\n * Declare whether a middleware authenticates the request. `authMiddleware()`\n * from `@warlock.js/auth` marks itself (and marks `{ optional: true }` as not\n * a guard); use this for a hand-written guard whose name does not contain\n * `auth` or `guard`.\n *\n * @example\n * export const requireApiKey = markAuthGuard(async ({ request, response }) => {\n * if (request.headers[\"x-api-key\"] !== env(\"API_KEY\")) return response.unauthorized();\n * });\n */\nexport function markAuthGuard<T extends (...args: never[]) => unknown>(\n middleware: T,\n isGuard = true,\n): T {\n Object.defineProperty(middleware, AUTH_GUARD_MARKER, {\n value: isGuard,\n configurable: true,\n enumerable: false,\n });\n\n return middleware;\n}\n\n/**\n * Whether a middleware authenticates the request: its {@link markAuthGuard}\n * marker when it has one, otherwise a function name containing `auth` or\n * `guard`.\n */\nexport function isAuthGuardMiddleware(middleware: unknown): boolean {\n if (typeof middleware !== \"function\") return false;\n\n const marker: unknown = Reflect.get(middleware, AUTH_GUARD_MARKER);\n if (typeof marker === \"boolean\") return marker;\n\n return isAuthGuardName(middleware.name);\n}\n\n/**\n * Whether an auth guard is in the route's middleware chain. `route.middleware`\n * already holds the middleware of every enclosing `router.group`, so a guard\n * declared once on a group counts for each route inside it.\n */\nexport function isRequiredGuardRoute(route: Pick<Route, \"middleware\">): boolean {\n return (route.middleware ?? []).some(isAuthGuardMiddleware);\n}\n\n/** Whether a route mutates state, is not a page route, and is neither guarded nor public. */\nexport function isUnguardedMutatingRoute(route: Route): boolean {\n if (!isMutatingHttpMethod(String(route.method))) return false;\n // Page actions are authorized by the page itself (`requireUser()`).\n if (route.isPage) return false;\n if (route.public === true) return false;\n\n return !isRequiredGuardRoute(route);\n}\n\n/** Routes already reported in this process, so an HMR reload does not repeat them. */\nconst reported = new Set<string>();\n\nfunction routeKey(route: Route): string {\n return `${String(route.method).toUpperCase()} ${route.path}`;\n}\n\n/**\n * Warn, once per route per process, about every mutating route with no auth\n * middleware and no `public: true`. Returns the routes it reported.\n */\nexport function reportUnguardedMutatingRoutes(\n routes: readonly Route[],\n logger: Pick<typeof log, \"warn\"> = log,\n): Route[] {\n const fresh = routes.filter(\n (route) => isUnguardedMutatingRoute(route) && !reported.has(routeKey(route)),\n );\n\n if (fresh.length === 0) return fresh;\n\n for (const route of fresh) reported.add(routeKey(route));\n\n const headline =\n fresh.length === 1\n ? \"1 mutating route has no auth middleware and is not marked public.\"\n : `${fresh.length} mutating routes have no auth middleware and are not marked public.`;\n\n logger.warn({\n module: \"router\",\n action: \"unguardedRoute\",\n message:\n `${headline} Anyone who can reach the server can call them. Add auth middleware ` +\n `(e.g. register them inside a guarded group), or pass { public: true } if they are ` +\n `meant to be open:\\n` +\n fresh\n .map((route) => ` - ${routeKey(route)}${route.sourceFile ? ` (${route.sourceFile})` : \"\"}`)\n .join(\"\\n\"),\n context: {\n routes: fresh.map((route) => ({\n method: route.method,\n path: route.path,\n name: route.name,\n sourceFile: route.sourceFile,\n })),\n },\n });\n\n return fresh;\n}\n\n/** Forget what was reported (tests). */\nexport function resetUnguardedRouteReports(): void {\n reported.clear();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAuBA,MAAa,oBAAmC,OAAO,IAAI,sBAAsB;;;;;;;;;;;;AAajF,SAAgB,cACd,YACA,UAAU,MACP;CACH,OAAO,eAAe,YAAY,mBAAmB;EACnD,OAAO;EACP,cAAc;EACd,YAAY;CACd,CAAC;CAED,OAAO;AACT;;;;;;AAOA,SAAgB,sBAAsB,YAA8B;CAClE,IAAI,OAAO,eAAe,YAAY,OAAO;CAE7C,MAAM,SAAkB,QAAQ,IAAI,YAAY,iBAAiB;CACjE,IAAI,OAAO,WAAW,WAAW,OAAO;CAExC,OAAO,gBAAgB,WAAW,IAAI;AACxC;;;;;;AAOA,SAAgB,qBAAqB,OAA2C;CAC9E,QAAQ,MAAM,cAAc,CAAC,EAAC,CAAE,KAAK,qBAAqB;AAC5D;;AAGA,SAAgB,yBAAyB,OAAuB;CAC9D,IAAI,CAAC,qBAAqB,OAAO,MAAM,MAAM,CAAC,GAAG,OAAO;CAExD,IAAI,MAAM,QAAQ,OAAO;CACzB,IAAI,MAAM,WAAW,MAAM,OAAO;CAElC,OAAO,CAAC,qBAAqB,KAAK;AACpC;;AAGA,MAAM,2BAAW,IAAI,IAAY;AAEjC,SAAS,SAAS,OAAsB;CACtC,OAAO,GAAG,OAAO,MAAM,MAAM,CAAC,CAAC,YAAY,EAAE,GAAG,MAAM;AACxD;;;;;AAMA,SAAgB,8BACd,QACA,SAAmC,KAC1B;CACT,MAAM,QAAQ,OAAO,QAClB,UAAU,yBAAyB,KAAK,KAAK,CAAC,SAAS,IAAI,SAAS,KAAK,CAAC,CAC7E;CAEA,IAAI,MAAM,WAAW,GAAG,OAAO;CAE/B,KAAK,MAAM,SAAS,OAAO,SAAS,IAAI,SAAS,KAAK,CAAC;CAEvD,MAAM,WACJ,MAAM,WAAW,IACb,sEACA,GAAG,MAAM,OAAO;CAEtB,OAAO,KAAK;EACV,QAAQ;EACR,QAAQ;EACR,SACE,GAAG,SAAS,6KAGZ,MACG,KAAK,UAAU,OAAO,SAAS,KAAK,IAAI,MAAM,aAAa,KAAK,MAAM,WAAW,KAAK,IAAI,CAAC,CAC3F,KAAK,IAAI;EACd,SAAS,EACP,QAAQ,MAAM,KAAK,WAAW;GAC5B,QAAQ,MAAM;GACd,MAAM,MAAM;GACZ,MAAM,MAAM;GACZ,YAAY,MAAM;EACpB,EAAE,EACJ;CACF,CAAC;CAED,OAAO;AACT;;AAGA,SAAgB,6BAAmC;CACjD,SAAS,MAAM;AACjB"}
|
|
@@ -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":";;;;;;AAiBA,MAAa,wBAAuC;CAClD,mCAAmB,IAAI,IAAI;CAC3B,mBAAmB;AACrB;;AAGA,MAAM,8BAAc,IAAI,IAAI,CAAC,WAAW,eAAe,CAAC;;AAGxD,SAAS,eAAe,QAAkB,MAAoC;CAC5E,MAAM,QAAQ,OAAO,MAAM;CAC3B,IAAI,MAAM,SAAS,oBAAoB,OAAO;CAC9C,KAAK,MAAM,YAAY,MAAM,YAAY;EACvC,IAAI,SAAS,SAAS,YAAY;EAClC,IAAIA,QAAY,SAAS,GAAG,MAAM,MAAM,OAAO,SAAS;CAC1D;AAEF;;;;;;;AAQA,SAAgB,yBAAyB,QAAkC;CACzE,MAAM,QAAkB,CAAC;CAEzB,KACE,SACC,SAAS;EACR,IAAI,KAAK,SAAS,kBAAkB;EACpC,KAAK,MAAM,YAAY,KAAK,WAAW;GACrC,MAAM,SAAS,eAAe,UAAU,eAAe;GACvD,MAAM,OAAO,WAAW,SAAY,SAAY,OAAO,MAAM;GAC7D,MAAM,OAAO,cAAc,eAAe,UAAU,MAAM,CAAC;GAC3D,IAAI,MAAM,SAAS,aAAa,KAAK,UAAU,SAAS,SAAS,QAC/D,MAAM,KAAK,IAAI;EACnB;CACF,GACA,IACF;CAEA,OAAO;AACT;;AAGA,SAAgB,oBAAoB,QAAiC;CACnE,IAAI,WAAW;CAEf,KACE,SACC,SAAS;EACR,IAAI,UAAU,OAAO;EACrB,IAAI,KAAK,SAAS,wBAAwBA,QAAY,KAAK,EAAE,MAAM,UAAU;EAC7E,IAAI,KAAK,SAAS,QAAQ,KAAK,SAAS,QAAW;EACnD,IAAI,eAAe,KAAK,MAAM,WAAW,MAAM,QAAW,WAAW;CACvE,GACA,IACF;CAEA,OAAO;AACT;;;;;;AAOA,SAAS,aAAa,MAA+D;CACnF,MAAM,QAAQ,OAAO,IAAI;CACzB,MAAM,UAAU,cAAc,KAAK;CACnC,IAAI,YAAY,QAAW,OAAO;EAAE,MAAM;EAAS,QAAQ;CAAM;CACjE,IAAI,MAAM,SAAS,qBAAqB,MAAM,YAAY,SAAS,GAAG;EACpE,MAAM,OAAO,MAAM,OAAO,EAAE,EAAE,MAAM,UAAU;EAC9C,OAAO,KAAK,SAAS,IAAI;GAAE,MAAM;GAAM,QAAQ;EAAK,IAAI;CAC1D;AAEF;;AAGA,SAAS,gBAAgB,MAAgD;CACvE,MAAM,QAAQ,KAAK,UAAU;CAC7B,IAAI,UAAU,QAAW,OAAO;CAChC,IAAI,WAAW,IAAI,MAAM,WAAW,OAAO;CAC3C,OAAO,eAAe,OAAO,OAAO;AACtC;;;;;;;;;;;;;AAcA,SAAgB,0BACd,QACA,QAAuB,uBACN;CACjB,IAAI,CAAC,+BAA+B,KAAK,OAAO,QAAQ,GAAG,OAAO,CAAC;CACnE,IAAI,kCAAkC,KAAK,OAAO,QAAQ,GAAG,OAAO,CAAC;CAErE,MAAM,WAA4B,CAAC;CAEnC,KACE,SACC,SAAS;EACR,IAAI,KAAK,SAAS,kBAAkB;EACpC,IAAI,CAAC,YAAY,IAAI,WAAW,IAAI,KAAK,EAAE,GAAG;EAC9C,MAAM,WAAW,gBAAgB,IAAI;EACrC,IAAI,aAAa,QAAW;EAC5B,MAAM,SAAS,aAAa,QAAQ;EAEpC,IAAI,WAAW,QAAW;GACxB,IAAI,CAAC,MAAM,mBACT,SAAS,KAAK;IACZ,MAAM;IACN,MAAM;IACN,UAAU;IACV,SACE;GACJ,CAAC;GACH;EACF;EAEA,MAAM,SAAS,CAAC,GAAG,MAAM,iBAAiB,CAAC,CAAC,MAAM,SAChD,OAAO,SAAS,KAAK,WAAW,OAAO,IAAI,IAAI,SAAS,OAAO,IACjE;EAEA,IAAI,WAAW,QACb,SAAS,KAAK;GACZ,MAAM;GACN,MAAM;GACN,UAAU;GACV,SAAS,UAAU,OAAO;EAC5B,CAAC;CACL,GACA,IACF;CAEA,OAAO;AACT;;;;;AAMA,SAAgB,qBAAqB,KAAa,OAAyC;CACzF,MAAM,oCAAoB,IAAI,IAAY;CAC1C,IAAI,oBAAoB;CAExB,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,CAAC,0BAA0B,KAAK,IAAI,GAAG;EAE3C,IAAI;EAEJ,IAAI;GACF,aAAaC,gBAAoB,MAAM,aAAa,KAAK,KAAK,KAAK,IAAI,GAAG,MAAM,CAAC;EACnF,QAAQ;GACN;EACF;EAEA,KAAK,MAAM,QAAQ,yBAAyB,UAAU,GAAG,kBAAkB,IAAI,IAAI;EACnF,IAAI,sCAAsC,KAAK,IAAI,KAAK,oBAAoB,UAAU,GACpF,oBAAoB;CACxB;CAEA,OAAO;EAAE;EAAmB;CAAkB;AAChD"}
|
|
@@ -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));
|