@webjsdev/cli 0.10.22 → 0.10.23
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/bin/webjs.js +1 -1
- package/lib/doctor.js +49 -1
- package/package.json +1 -1
- package/templates/CONVENTIONS.md +4 -3
package/bin/webjs.js
CHANGED
|
@@ -47,7 +47,7 @@ const USAGE = `webjs commands:
|
|
|
47
47
|
webjs test [--server|--browser] Run server + browser tests
|
|
48
48
|
webjs check [--json] Run correctness checks on the app (--json emits structured violations)
|
|
49
49
|
webjs mcp Start the read-only MCP server (routes / actions / components / check)
|
|
50
|
-
webjs doctor Verify project health (Node, tsconfig, env, vendor pins, importmap coherence, @webjsdev versions, git hook)
|
|
50
|
+
webjs doctor Verify project health (Node, tsconfig, env, vendor pins, importmap coherence, @webjsdev versions, git hook, page/layout elision)
|
|
51
51
|
webjs types Generate .webjs/routes.d.ts (typed Route union + per-route params)
|
|
52
52
|
webjs typecheck [tsc args...] Type-check the app with the project's tsc --noEmit (non-zero on errors)
|
|
53
53
|
webjs create <name> [--template full-stack|api|saas] [--db sqlite|postgres] [--runtime node|bun] [--no-install] Scaffold a new webjs app
|
package/lib/doctor.js
CHANGED
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
import { existsSync, statSync } from 'node:fs';
|
|
38
38
|
import { readFile } from 'node:fs/promises';
|
|
39
|
-
import { join } from 'node:path';
|
|
39
|
+
import { join, relative } from 'node:path';
|
|
40
40
|
import { checkNodeInline } from './node-preflight.js';
|
|
41
41
|
|
|
42
42
|
/**
|
|
@@ -795,6 +795,53 @@ function checkGitHook(appDir) {
|
|
|
795
795
|
* instead of a real live resolve / node_modules read.
|
|
796
796
|
* @returns {Promise<DoctorResult[]>}
|
|
797
797
|
*/
|
|
798
|
+
/**
|
|
799
|
+
* Advisory (#646): name why a page/layout SHIPS its module to the browser
|
|
800
|
+
* instead of being elided. A page/layout that is a pure carrier (import-only
|
|
801
|
+
* #605 / inert #179) stays out of the browser; one that ships whole is pinned
|
|
802
|
+
* by a specific client-effecting NON-component in its closure (a util touching
|
|
803
|
+
* a client global, a module-scope side effect, a bare side-effect import) or by
|
|
804
|
+
* its own client work. This turns that invisible #605/#179 regression into a
|
|
805
|
+
* named line. WARN only: a page legitimately MAY ship, and the analyser is
|
|
806
|
+
* biased toward shipping by design (server AGENTS invariant 7), so this is a
|
|
807
|
+
* "you may not have intended this" hint, never a hard fail.
|
|
808
|
+
* @param {string} appDir
|
|
809
|
+
* @returns {Promise<DoctorResult>}
|
|
810
|
+
*/
|
|
811
|
+
async function checkElisionCarriers(appDir) {
|
|
812
|
+
const name = 'Page/layout elision (carrier hygiene)';
|
|
813
|
+
let report;
|
|
814
|
+
try {
|
|
815
|
+
const { analyzeAppElision } = await import('@webjsdev/server');
|
|
816
|
+
report = await analyzeAppElision(appDir);
|
|
817
|
+
} catch {
|
|
818
|
+
// Analysis unavailable (no app, malformed, server import failed): no advice.
|
|
819
|
+
return { name, status: 'pass', message: 'not analysed (no routable app or analysis unavailable)' };
|
|
820
|
+
}
|
|
821
|
+
if (!report.analysed) {
|
|
822
|
+
return { name, status: 'pass', message: 'not analysed (no routable app, or elision is disabled)' };
|
|
823
|
+
}
|
|
824
|
+
if (report.shipped.length === 0) {
|
|
825
|
+
return { name, status: 'pass', message: 'every page/layout is elided (a pure import-only or inert carrier)' };
|
|
826
|
+
}
|
|
827
|
+
const rel = (f) => relative(appDir, f) || f;
|
|
828
|
+
// Name the FIRST client-effecting blocker (there may be more than one; the
|
|
829
|
+
// module stays shipped until every such blocker is moved out).
|
|
830
|
+
const lines = report.shipped.map(({ file, blocker, reason }) =>
|
|
831
|
+
blocker
|
|
832
|
+
? `${rel(file)} ships whole. Its first client-effecting blocker is ${rel(blocker)}, which ${reason} and is not a component`
|
|
833
|
+
: `${rel(file)} ships whole because it ${reason}`,
|
|
834
|
+
);
|
|
835
|
+
return {
|
|
836
|
+
name,
|
|
837
|
+
status: 'warn',
|
|
838
|
+
message:
|
|
839
|
+
`${report.shipped.length} page/layout module(s) ship to the browser instead of being elided:\n` +
|
|
840
|
+
lines.map((l) => ` ${l}`).join('\n'),
|
|
841
|
+
fix: 'Move the client work out of the page/layout closure (into a component, or a .server module reached through an action) so the carrier can be elided, or accept that it ships. See agent-docs/components.md.',
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
|
|
798
845
|
export async function runDoctorChecks(appDir, opts = {}) {
|
|
799
846
|
const cliDir = opts.cliDir || new URL('.', import.meta.url).pathname;
|
|
800
847
|
const results = await Promise.all([
|
|
@@ -806,6 +853,7 @@ export async function runDoctorChecks(appDir, opts = {}) {
|
|
|
806
853
|
checkWebjsVersions(appDir),
|
|
807
854
|
checkImportmapCoherence(appDir, opts),
|
|
808
855
|
Promise.resolve(checkGitHook(appDir)),
|
|
856
|
+
checkElisionCarriers(appDir),
|
|
809
857
|
]);
|
|
810
858
|
return results;
|
|
811
859
|
}
|
package/package.json
CHANGED
package/templates/CONVENTIONS.md
CHANGED
|
@@ -506,7 +506,7 @@ SSR, page actions, server-action RPC, auth + CSRF), drive
|
|
|
506
506
|
|
|
507
507
|
```ts
|
|
508
508
|
import { createRequestHandler } from '@webjsdev/server';
|
|
509
|
-
import { testRequest,
|
|
509
|
+
import { testRequest, invokeActionForTest, loginAndGetCookies, withSessionCookie }
|
|
510
510
|
from '@webjsdev/server/testing';
|
|
511
511
|
|
|
512
512
|
const app = await createRequestHandler({ appDir: process.cwd(), dev: true });
|
|
@@ -524,8 +524,9 @@ const out = await invokeActionForTest(app, 'modules/posts/actions/create.server.
|
|
|
524
524
|
|
|
525
525
|
Prefer `invokeActionForTest` over a direct import of the action when you want
|
|
526
526
|
to verify the production contract: it exercises the wire serializer (a `Date` /
|
|
527
|
-
`Map` arg survives),
|
|
528
|
-
|
|
527
|
+
`Map` arg survives), the Origin / Sec-Fetch-Site CSRF check (it models a
|
|
528
|
+
same-origin POST), and prod error sanitization, which a direct call bypasses.
|
|
529
|
+
The saas template's `test/auth/auth.test.ts` is a worked example.
|
|
529
530
|
|
|
530
531
|
This is also why the auth test lives at `test/auth/auth.test.ts` (the
|
|
531
532
|
feature-folder convention), NOT `test/unit/auth.test.ts`. Test KIND is a
|