@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/server",
3
- "version": "0.8.51",
3
+ "version": "0.8.53",
4
4
  "type": "module",
5
5
  "description": "webjs dev/prod server: SSR, router, API, server actions, live reload",
6
6
  "main": "index.js",
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
- // Walk the body, tracking brace depth. At depth 0, look for
244
- // class-field-shaped lines.
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 < classBody.length) {
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 < classBody.length && classBody[i] !== '\n') 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 < classBody.length && !(classBody[i] === '*' && classBody[i + 1] === '/')) 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
@@ -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
- * agent-docs/components.md.
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 agent-docs/components.md. `sigil-coverage.test.js`
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 agent-docs/components.md and server AGENTS invariant 7;
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.)