@webjsdev/server 0.8.51 → 0.8.53
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/package.json +1 -1
- package/src/check.js +42 -72
- package/src/component-elision.js +3 -3
package/package.json
CHANGED
package/src/check.js
CHANGED
|
@@ -129,11 +129,6 @@ export const RULES = [
|
|
|
129
129
|
description:
|
|
130
130
|
'A `\'use server\'` action file that declares HTTP-verb config (any of `method` / `cache` / `tags` / `invalidates` / `validate`, #488) must export exactly ONE callable action function. The config is file-level (it applies to the action in the file), so a second exported function would silently inherit the same verb / cache, which is almost never intended and makes the contract ambiguous. Move the extra function to its own `.server.ts` file (the one-function-per-file convention), or, if it is a private helper, do not export it. Files with no verb config are unaffected.',
|
|
131
131
|
},
|
|
132
|
-
{
|
|
133
|
-
name: 'no-scaffold-placeholder',
|
|
134
|
-
description:
|
|
135
|
-
'The one sentinel-based check, and the deliberate exception to the "objectively broken code" framing of the other rules: it flags scaffold example content that has not been replaced, so a delivered app contains only what the user intended and never leftover scaffold code. `webjs create` marks its example/demo files (the example homepage in app/page.ts, the example app chrome in app/layout.ts) with a one-line marker comment carrying the literal token `webjs-scaffold-placeholder`. The rule scans raw source (the marker lives in a comment, which the redacted scan view would hide) and fails for every file that still carries the token. A freshly scaffolded app fails this rule BY DESIGN until its placeholders are addressed: replace the example content, or deliberately keep it, and in either case delete the marker line. No finished app legitimately ships a literal remove-me marker, so this still qualifies as a check rather than a convention. The marker is acknowledge-and-remove, so keeping a piece is a one-line deletion rather than a forced rewrite.',
|
|
136
|
-
},
|
|
137
132
|
{
|
|
138
133
|
name: 'no-redirect-in-api-route',
|
|
139
134
|
description:
|
|
@@ -240,74 +235,70 @@ function isArrayTypeText(type) {
|
|
|
240
235
|
function findFieldInitializers(classBody, props) {
|
|
241
236
|
/** @type {string[]} */
|
|
242
237
|
const out = [];
|
|
243
|
-
|
|
244
|
-
// class-field
|
|
238
|
+
const n = classBody.length;
|
|
239
|
+
// Match class-field declarations. Two shapes:
|
|
240
|
+
// 1. With initializer: `count = 0`, `count: number = 0`, `public count = 0`
|
|
241
|
+
// 2. Type-only (no initializer): `count!: number`, `count?: number`, `count: number`
|
|
242
|
+
// Both compile to Object.defineProperty after super() under modern class-field
|
|
243
|
+
// semantics, clobbering the reactive accessor.
|
|
244
|
+
// The initializer regex: optional modifier, identifier, optional type, then `=`.
|
|
245
|
+
const initRe = /^\s*(?:(public|private|protected|readonly)\s+)?([A-Za-z_$][\w$]*)\s*(?::\s*[^=;]+)?\s*=\s*[^=>]/;
|
|
246
|
+
// The type-only regex: optional modifier, identifier, then `!:` or `?:` or `:` with a type.
|
|
247
|
+
const typeOnlyRe = /^\s*(?:(public|private|protected|readonly)\s+)?([A-Za-z_$][\w$]*)\s*[!?]?\s*:\s*\S/;
|
|
248
|
+
const examineLine = (lineStart) => {
|
|
249
|
+
let j = lineStart;
|
|
250
|
+
while (j < n && classBody[j] !== '\n') j++;
|
|
251
|
+
const line = classBody.slice(lineStart, j);
|
|
252
|
+
const initM = initRe.exec(line);
|
|
253
|
+
const typeM = typeOnlyRe.exec(line);
|
|
254
|
+
// Prefer the initializer match; if neither, skip.
|
|
255
|
+
const name = initM ? initM[2] : (typeM ? typeM[2] : null);
|
|
256
|
+
// `declare`, `static`, and `this.` patterns shouldn't reach here
|
|
257
|
+
// (declare/static start with their keyword, this.x has the dot in
|
|
258
|
+
// the regex group), but guard against matching keywords as names:
|
|
259
|
+
if (name && name !== 'declare' && name !== 'static' && props.has(name)) out.push(name);
|
|
260
|
+
};
|
|
261
|
+
// Walk the body char by char, tracking brace depth. A class field lives at
|
|
262
|
+
// the class-body top level (depth 0). We examine a line ONCE, at its first
|
|
263
|
+
// non-whitespace char, only while depth is 0, and WITHOUT skipping the braces
|
|
264
|
+
// on that line: an opening brace on the line (`method() {`, `field = {`) must
|
|
265
|
+
// still be counted so the lines inside its block are seen at depth > 0. The
|
|
266
|
+
// earlier version jumped `i` past the whole examined line, which dropped that
|
|
267
|
+
// line's braces and let object-literal keys inside a method body (`{ game:
|
|
268
|
+
// ..., scoreboard: ... }`) be misread as depth-0 class fields (#934).
|
|
245
269
|
let depth = 0;
|
|
246
270
|
let i = 0;
|
|
247
271
|
let lineStart = 0;
|
|
272
|
+
let examined = false;
|
|
248
273
|
let str = '';
|
|
249
|
-
while (i <
|
|
274
|
+
while (i < n) {
|
|
250
275
|
const c = classBody[i];
|
|
251
276
|
if (str) {
|
|
252
277
|
if (c === '\\') { i += 2; continue; }
|
|
253
278
|
if (c === str) str = '';
|
|
254
|
-
else if (str === '`' && c === '$' && classBody[i + 1] === '{') {
|
|
255
|
-
depth++;
|
|
256
|
-
i += 2;
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
i++;
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
if (c === '\n') {
|
|
263
|
-
lineStart = i + 1;
|
|
264
279
|
i++;
|
|
265
280
|
continue;
|
|
266
281
|
}
|
|
282
|
+
if (c === '\n') { lineStart = i + 1; examined = false; i++; continue; }
|
|
267
283
|
if (c === '/' && classBody[i + 1] === '/') {
|
|
268
|
-
while (i <
|
|
284
|
+
while (i < n && classBody[i] !== '\n') i++;
|
|
269
285
|
continue;
|
|
270
286
|
}
|
|
271
287
|
if (c === '/' && classBody[i + 1] === '*') {
|
|
272
288
|
i += 2;
|
|
273
|
-
while (i <
|
|
289
|
+
while (i < n && !(classBody[i] === '*' && classBody[i + 1] === '/')) i++;
|
|
274
290
|
i += 2;
|
|
275
291
|
continue;
|
|
276
292
|
}
|
|
277
293
|
if (c === "'" || c === '"' || c === '`') { str = c; i++; continue; }
|
|
294
|
+
// First non-whitespace char of a class-body top-level line: examine it for
|
|
295
|
+
// a field declaration BEFORE consuming any brace it opens.
|
|
296
|
+
if (depth === 0 && !examined && !/\s/.test(c)) {
|
|
297
|
+
examined = true;
|
|
298
|
+
examineLine(lineStart);
|
|
299
|
+
}
|
|
278
300
|
if (c === '{') { depth++; i++; continue; }
|
|
279
301
|
if (c === '}') { depth--; i++; continue; }
|
|
280
|
-
// At class-body top level, examine candidate lines starting at lineStart.
|
|
281
|
-
if (depth === 0 && i === lineStart || (depth === 0 && /\s/.test(c) && i === lineStart)) {
|
|
282
|
-
// Take the rest of the line up to a newline.
|
|
283
|
-
let j = lineStart;
|
|
284
|
-
while (j < classBody.length && classBody[j] !== '\n') j++;
|
|
285
|
-
const line = classBody.slice(lineStart, j);
|
|
286
|
-
// Match class-field declarations. Two shapes:
|
|
287
|
-
// 1. With initializer: `count = 0`, `count: number = 0`, `public count = 0`
|
|
288
|
-
// 2. Type-only (no initializer): `count!: number`, `count?: number`, `count: number`
|
|
289
|
-
// Both compile to Object.defineProperty after super() under modern class-field
|
|
290
|
-
// semantics, clobbering the reactive accessor.
|
|
291
|
-
// The initializer regex: optional modifier, identifier, optional type, then `=`.
|
|
292
|
-
const initRe = /^\s*(?:(public|private|protected|readonly)\s+)?([A-Za-z_$][\w$]*)\s*(?::\s*[^=;]+)?\s*=\s*[^=>]/;
|
|
293
|
-
// The type-only regex: optional modifier, identifier, then `!:` or `?:` or `:` with a type.
|
|
294
|
-
const typeOnlyRe = /^\s*(?:(public|private|protected|readonly)\s+)?([A-Za-z_$][\w$]*)\s*[!?]?\s*:\s*\S/;
|
|
295
|
-
const initM = initRe.exec(line);
|
|
296
|
-
const typeM = typeOnlyRe.exec(line);
|
|
297
|
-
// Prefer the initializer match; if neither, skip.
|
|
298
|
-
const name = initM ? initM[2] : (typeM ? typeM[2] : null);
|
|
299
|
-
if (name) {
|
|
300
|
-
// `declare`, `static`, and `this.` patterns shouldn't reach here
|
|
301
|
-
// (declare/static start with their keyword, this.x has the dot in
|
|
302
|
-
// the regex group), but guard against matching keywords as names:
|
|
303
|
-
if (name !== 'declare' && name !== 'static' && props.has(name)) {
|
|
304
|
-
out.push(name);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
// Advance past this line so we don't re-match.
|
|
308
|
-
i = j;
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
302
|
i++;
|
|
312
303
|
}
|
|
313
304
|
return out;
|
|
@@ -668,27 +659,6 @@ export async function checkConventions(appDir) {
|
|
|
668
659
|
}
|
|
669
660
|
}
|
|
670
661
|
|
|
671
|
-
// --- Rule: no-scaffold-placeholder ---
|
|
672
|
-
// `webjs create` marks its example/demo files (app/page.ts, app/layout.ts)
|
|
673
|
-
// with a one-line marker comment so a delivered app cannot silently ship
|
|
674
|
-
// leftover scaffold content. Scan RAW `content`: the marker lives in a
|
|
675
|
-
// comment, which the `scan` view redacts to whitespace. One violation per
|
|
676
|
-
// still-marked file. The token is assembled here so this source does not
|
|
677
|
-
// itself carry the contiguous literal.
|
|
678
|
-
{
|
|
679
|
-
const MARKER = 'webjs-scaffold-' + 'placeholder';
|
|
680
|
-
for (const { rel, content } of files) {
|
|
681
|
-
if (!content.includes(MARKER)) continue;
|
|
682
|
-
violations.push({
|
|
683
|
-
rule: 'no-scaffold-placeholder',
|
|
684
|
-
file: rel,
|
|
685
|
-
message:
|
|
686
|
-
'Scaffold placeholder marker still present. This file is unmodified example content from `webjs create`, and the delivered app should contain only what the user intended, not leftover scaffold code.',
|
|
687
|
-
fix: `Replace the example content (or deliberately keep it), then delete the marker comment line carrying the ${MARKER} token.`,
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
662
|
// --- Rule: no-redirect-in-api-route ---
|
|
693
663
|
// `redirect()` from `@webjsdev/core` throws a control-flow signal designed
|
|
694
664
|
// for the SSR page renderer. In a `route.ts` API handler it goes uncaught
|
package/src/component-elision.js
CHANGED
|
@@ -93,7 +93,7 @@ export const REACTIVE_IMPORTS = new Set([
|
|
|
93
93
|
* defines `render`, and the SSR walker calls it directly. `constructor`
|
|
94
94
|
* is absent for the same reason (it runs during SSR to seed first
|
|
95
95
|
* paint). Keep this list in lockstep with the lifecycle table in
|
|
96
|
-
*
|
|
96
|
+
* the skill's references/components.md.
|
|
97
97
|
*
|
|
98
98
|
* @type {readonly string[]}
|
|
99
99
|
*/
|
|
@@ -134,7 +134,7 @@ export const CLIENT_METHOD_CALLS = ['addController', 'removeController', 'reques
|
|
|
134
134
|
* `lifecycle-coverage.test.js` via prototype introspection), there is no
|
|
135
135
|
* enumerable runtime source of "all static conventions", so the contract is
|
|
136
136
|
* a documented one: a new interactivity static field MUST be added here AND
|
|
137
|
-
* to the lifecycle table in
|
|
137
|
+
* to the lifecycle table in the skill's references/components.md. `sigil-coverage.test.js`
|
|
138
138
|
* asserts each entry is honoured as a ship signal.
|
|
139
139
|
*
|
|
140
140
|
* - `shadow`: Declarative Shadow DOM attaches ONLY during HTML parsing, so a
|
|
@@ -448,7 +448,7 @@ function importsClientRouter(src, literals) {
|
|
|
448
448
|
* `customElements.define` at module top level) imported via a binding clause
|
|
449
449
|
* is NOT caught here, so eliding the importer drops that registration and the
|
|
450
450
|
* element silently does not upgrade. This is the cross-module-registration
|
|
451
|
-
* caveat documented in
|
|
451
|
+
* caveat documented in the skill's references/components.md and server AGENTS invariant 7;
|
|
452
452
|
* the fix is `.server.{js,ts}` for genuinely server-only deps, or an
|
|
453
453
|
* interactivity signal on the consumer. (It is not caught by an SSR crash:
|
|
454
454
|
* the SSR `customElements` shim makes `define` a no-op server-side.)
|