@webjsdev/server 0.8.31 → 0.8.33
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 +112 -66
- package/src/component-elision.js +259 -52
- package/src/component-scanner.js +15 -10
- package/src/dev.js +56 -4
- package/src/js-scan.js +292 -10
- package/src/ssr.js +38 -4
- package/src/vendor.js +4 -1
- package/webjs-config.schema.json +5 -0
package/package.json
CHANGED
package/src/check.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
redactStringsAndTemplates,
|
|
6
6
|
extractWebComponentClassBodies,
|
|
7
7
|
matchClosingBrace,
|
|
8
|
+
parsePropEntries,
|
|
8
9
|
} from './js-scan.js';
|
|
9
10
|
import { buildModuleGraph, transitiveDeps } from './module-graph.js';
|
|
10
11
|
import { scanComponents } from './component-scanner.js';
|
|
@@ -74,9 +75,19 @@ export const RULES = [
|
|
|
74
75
|
'A custom-element tag name must be registered exactly once across the app. Two `Class.register(\'tag\')` / `customElements.define(\'tag\', …)` calls for the SAME tag resolve INCONSISTENTLY at runtime: SSR overwrites the registry (last registration wins) while the browser keeps the first native upgrade, so the rendered element and the webjs registry disagree. Rename one tag.',
|
|
75
76
|
},
|
|
76
77
|
{
|
|
77
|
-
name: '
|
|
78
|
+
name: 'no-static-properties',
|
|
78
79
|
description:
|
|
79
|
-
'Reactive properties
|
|
80
|
+
'Reactive properties must be declared via the `extends WebComponent({ … })` factory, never a hand-written `static properties = { … }` field in the class body. The factory types each field for you (no `declare` needed) and the runtime throws on a direct `static properties`. Migrate `class X extends WebComponent { static properties = { count: { type: Number } } }` to `class X extends WebComponent({ count: Number })`; use the `prop()` helper for options (`prop(Number, { reflect: true })`) and set defaults via the `default` option or in the constructor.',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'reactive-props-no-class-field',
|
|
84
|
+
description:
|
|
85
|
+
'A reactive property declared via the `extends WebComponent({ … })` factory must NOT also have a plain class-field initializer (`count = 0` or `count: number = 0`) in the class body. Under modern class-field semantics that initializer runs Object.defineProperty *after* super(), clobbering the framework\'s reactive accessor and silently breaking re-renders. Set the default via the `default` property option (`prop(Number, { default: 0 })`) or by assigning in the constructor after super().',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'array-prop-uses-array-type',
|
|
89
|
+
description:
|
|
90
|
+
'An array-typed reactive property declared via the `extends WebComponent({ … })` factory must pass the `Array` runtime constructor, not `Object`: `count: prop<Tag[]>(Array)`, never `prop<Tag[]>(Object)`. The two share one converter (both JSON-encode the value), so the wrong one does not crash, but `Object` misstates the prop contract to the next reader and diverges from the documented built-in set (String/Number/Boolean/Object/Array). Fires only when the factory generic is itself an array type (`T[]`, `readonly T[]`, `Array<T>`, `ReadonlyArray<T>`) AND the constructor argument is `Object`; a bare `foo: Object` with no generic is never flagged. Fix: change the constructor to `Array`.',
|
|
80
91
|
},
|
|
81
92
|
{
|
|
82
93
|
name: 'shell-in-non-root-layout',
|
|
@@ -166,58 +177,6 @@ function isComponentFile(relPath) {
|
|
|
166
177
|
return segments.includes('components');
|
|
167
178
|
}
|
|
168
179
|
|
|
169
|
-
/**
|
|
170
|
-
* Find every `<key>:` entry inside the first `static properties = { … }`
|
|
171
|
-
* literal in `classBody`. Returns the bare property names: the keys
|
|
172
|
-
* we'll then look up as class fields.
|
|
173
|
-
*
|
|
174
|
-
* @param {string} classBody
|
|
175
|
-
* @returns {Set<string>}
|
|
176
|
-
*/
|
|
177
|
-
function extractStaticPropertyNames(classBody) {
|
|
178
|
-
/** @type {Set<string>} */
|
|
179
|
-
const names = new Set();
|
|
180
|
-
const m = /static\s+properties\s*=\s*\{/.exec(classBody);
|
|
181
|
-
if (!m) return names;
|
|
182
|
-
const objStart = m.index + m[0].length;
|
|
183
|
-
const objEnd = matchClosingBrace(classBody, objStart);
|
|
184
|
-
if (objEnd === -1) return names;
|
|
185
|
-
const obj = classBody.slice(objStart, objEnd);
|
|
186
|
-
// Match keys at the top level of the object literal. A nested `{ … }`
|
|
187
|
-
// (the per-property declaration) is skipped via brace counting.
|
|
188
|
-
let i = 0;
|
|
189
|
-
while (i < obj.length) {
|
|
190
|
-
// Skip whitespace and commas.
|
|
191
|
-
while (i < obj.length && /[\s,]/.test(obj[i])) i++;
|
|
192
|
-
if (i >= obj.length) break;
|
|
193
|
-
// Read the identifier or string-literal key.
|
|
194
|
-
let key = '';
|
|
195
|
-
if (obj[i] === '"' || obj[i] === "'") {
|
|
196
|
-
const quote = obj[i++];
|
|
197
|
-
while (i < obj.length && obj[i] !== quote) { key += obj[i++]; }
|
|
198
|
-
i++; // closing quote
|
|
199
|
-
} else {
|
|
200
|
-
while (i < obj.length && /[A-Za-z0-9_$]/.test(obj[i])) key += obj[i++];
|
|
201
|
-
}
|
|
202
|
-
// Skip whitespace, then expect `:`.
|
|
203
|
-
while (i < obj.length && /\s/.test(obj[i])) i++;
|
|
204
|
-
if (obj[i] !== ':') break;
|
|
205
|
-
i++;
|
|
206
|
-
// Skip whitespace, then skip the value (either a `{ … }` literal or
|
|
207
|
-
// a single token like `String`).
|
|
208
|
-
while (i < obj.length && /\s/.test(obj[i])) i++;
|
|
209
|
-
if (obj[i] === '{') {
|
|
210
|
-
const valEnd = matchClosingBrace(obj, i + 1);
|
|
211
|
-
if (valEnd === -1) break;
|
|
212
|
-
i = valEnd + 1;
|
|
213
|
-
} else {
|
|
214
|
-
while (i < obj.length && obj[i] !== ',' && obj[i] !== '}') i++;
|
|
215
|
-
}
|
|
216
|
-
if (key) names.add(key);
|
|
217
|
-
}
|
|
218
|
-
return names;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
180
|
/**
|
|
222
181
|
* Scan a class body for class-field initializers naming any of `props`.
|
|
223
182
|
* "Class-field" means: at the top of the class body (brace depth 0
|
|
@@ -231,6 +190,38 @@ function extractStaticPropertyNames(classBody) {
|
|
|
231
190
|
* @param {Set<string>} props
|
|
232
191
|
* @returns {string[]}
|
|
233
192
|
*/
|
|
193
|
+
/**
|
|
194
|
+
* True when a factory prop value is an array-typed `prop<…>(…)` whose
|
|
195
|
+
* runtime constructor argument is `Object`. The generic and the
|
|
196
|
+
* constructor sit in the same call, so it is decidable from the value
|
|
197
|
+
* text alone. The match is greedy on the generic so a nested generic
|
|
198
|
+
* (`prop<Array<X>>(Object)`) closes at the outer `>` that precedes the
|
|
199
|
+
* `(Object` call. A bare constructor (`Object`, with no generic) or a
|
|
200
|
+
* non-array generic (`prop<Foo>(Object)`) returns false.
|
|
201
|
+
*
|
|
202
|
+
* @param {string} value the raw prop value text, e.g. `prop<Tag[]>(Object)`
|
|
203
|
+
* @returns {boolean}
|
|
204
|
+
*/
|
|
205
|
+
function arrayPropUsesObject(value) {
|
|
206
|
+
const m = /^prop\s*<([\s\S]*)>\s*\(\s*Object\s*[,)]/.exec(value.trim());
|
|
207
|
+
if (!m) return false;
|
|
208
|
+
return isArrayTypeText(m[1]);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* True when a TypeScript type expression denotes an array: `T[]`,
|
|
213
|
+
* `readonly T[]`, `T[][]`, `Array<T>`, or `ReadonlyArray<T>`.
|
|
214
|
+
*
|
|
215
|
+
* @param {string} type
|
|
216
|
+
* @returns {boolean}
|
|
217
|
+
*/
|
|
218
|
+
function isArrayTypeText(type) {
|
|
219
|
+
const bare = type.trim().replace(/^readonly\s+/, '');
|
|
220
|
+
if (/\[\s*\]$/.test(bare)) return true;
|
|
221
|
+
if (/^(?:Readonly)?Array\s*<[\s\S]*>$/.test(bare)) return true;
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
|
|
234
225
|
function findFieldInitializers(classBody, props) {
|
|
235
226
|
/** @type {string[]} */
|
|
236
227
|
const out = [];
|
|
@@ -440,23 +431,78 @@ export async function checkConventions(appDir) {
|
|
|
440
431
|
}
|
|
441
432
|
}
|
|
442
433
|
|
|
443
|
-
// --- Rule:
|
|
434
|
+
// --- Rule: no-static-properties ---
|
|
435
|
+
// A hand-written `static properties = { … }` in a WebComponent class body is
|
|
436
|
+
// no longer supported: reactive properties are declared via the
|
|
437
|
+
// `extends WebComponent({ … })` factory (the runtime throws on a direct
|
|
438
|
+
// `static properties`). Flag it statically so the editor catches it before
|
|
439
|
+
// the page 500s.
|
|
444
440
|
{
|
|
445
441
|
for (const { rel, scan } of files) {
|
|
446
|
-
// Use redacted source so
|
|
447
|
-
// `class X extends WebComponent {
|
|
448
|
-
// literals don't trip the rule. Real declarations live at
|
|
442
|
+
// Use redacted source so fixture-style strings like
|
|
443
|
+
// `class X extends WebComponent { static properties = {…} }` inside
|
|
444
|
+
// template literals don't trip the rule. Real declarations live at
|
|
449
445
|
// top-level code where the redactor leaves them alone.
|
|
450
446
|
if (!/class\s+\w+\s+extends\s+WebComponent/.test(scan)) continue;
|
|
451
|
-
for (const body of extractWebComponentClassBodies(scan)) {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
447
|
+
for (const { body } of extractWebComponentClassBodies(scan)) {
|
|
448
|
+
if (!/static\s+properties\s*=\s*\{/.test(body)) continue;
|
|
449
|
+
violations.push({
|
|
450
|
+
rule: 'no-static-properties',
|
|
451
|
+
file: rel,
|
|
452
|
+
message:
|
|
453
|
+
'`static properties = { … }` is no longer supported; declare reactive properties via the `extends WebComponent({ … })` factory instead.',
|
|
454
|
+
fix: 'Move the properties into the factory call: `class X extends WebComponent({ count: Number })`. Use `prop(Number, { reflect: true })` for options and set defaults via the `default` option or the constructor. Delete the `static properties` block and any `declare` fields for those props.',
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// --- Rule: reactive-props-no-class-field ---
|
|
461
|
+
// A reactive property declared via the factory must not also carry a plain
|
|
462
|
+
// class-field initializer: it runs Object.defineProperty after super() and
|
|
463
|
+
// clobbers the framework's reactive accessor (silent broken re-renders).
|
|
464
|
+
{
|
|
465
|
+
for (const { rel, scan } of files) {
|
|
466
|
+
if (!/class\s+\w+\s+extends\s+WebComponent/.test(scan)) continue;
|
|
467
|
+
for (const { body, factoryProps } of extractWebComponentClassBodies(scan)) {
|
|
468
|
+
if (factoryProps.size === 0) continue;
|
|
469
|
+
for (const bad of findFieldInitializers(body, factoryProps)) {
|
|
455
470
|
violations.push({
|
|
456
|
-
rule: 'reactive-props-
|
|
471
|
+
rule: 'reactive-props-no-class-field',
|
|
457
472
|
file: rel,
|
|
458
473
|
message: `Reactive prop \`${bad}\` uses a class-field initializer; this clobbers the framework's reactive accessor under modern class-field semantics.`,
|
|
459
|
-
fix: `
|
|
474
|
+
fix: `Remove the class-field initializer and set the default via the \`default\` option (\`prop(<Type>, { default: <value> })\`) or by assigning \`this.${bad} = <value>\` inside \`constructor()\` after \`super()\`.`,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// --- Rule: array-prop-uses-array-type ---
|
|
482
|
+
// An array-typed reactive prop declared via the factory should pass the
|
|
483
|
+
// `Array` runtime constructor, not `Object`. Both share one converter
|
|
484
|
+
// (JSON encode/decode), so `Object` does not crash, but it misstates the
|
|
485
|
+
// prop contract and diverges from the documented built-in set. Fires only
|
|
486
|
+
// when the factory generic is itself an array type AND the constructor is
|
|
487
|
+
// `Object`; a bare `foo: Object` (no generic to prove array-ness) is left
|
|
488
|
+
// alone to avoid false positives. Uses the redacted `scan`, so a
|
|
489
|
+
// `prop<X[]>(Object)` shown inside an html`` example string never fires.
|
|
490
|
+
{
|
|
491
|
+
for (const { rel, scan } of files) {
|
|
492
|
+
if (!/class\s+\w+\s+extends\s+WebComponent/.test(scan)) continue;
|
|
493
|
+
for (const { factoryArg } of extractWebComponentClassBodies(scan)) {
|
|
494
|
+
const objStart = factoryArg.indexOf('{');
|
|
495
|
+
if (objStart === -1) continue;
|
|
496
|
+
const objEnd = matchClosingBrace(factoryArg, objStart + 1);
|
|
497
|
+
if (objEnd === -1) continue;
|
|
498
|
+
const objContent = factoryArg.slice(objStart + 1, objEnd);
|
|
499
|
+
for (const { key, value } of parsePropEntries(objContent)) {
|
|
500
|
+
if (!arrayPropUsesObject(value)) continue;
|
|
501
|
+
violations.push({
|
|
502
|
+
rule: 'array-prop-uses-array-type',
|
|
503
|
+
file: rel,
|
|
504
|
+
message: `Array-typed reactive prop \`${key}\` is declared with the \`Object\` constructor (\`prop<…[]>(Object)\`); use \`Array\` so the runtime converter matches the declared shape.`,
|
|
505
|
+
fix: `Change the constructor to \`Array\`: \`${key}: prop<…[]>(Array)\`. Object and Array share one converter so behaviour is unchanged, but \`Array\` states the prop's shape correctly.`,
|
|
460
506
|
});
|
|
461
507
|
}
|
|
462
508
|
}
|
|
@@ -473,7 +519,7 @@ export async function checkConventions(appDir) {
|
|
|
473
519
|
{
|
|
474
520
|
for (const { rel, scan } of files) {
|
|
475
521
|
if (!/class\s+\w+\s+extends\s+WebComponent/.test(scan)) continue;
|
|
476
|
-
for (const body of extractWebComponentClassBodies(scan)) {
|
|
522
|
+
for (const { body } of extractWebComponentClassBodies(scan)) {
|
|
477
523
|
for (const method of ['constructor', 'willUpdate', 'render']) {
|
|
478
524
|
const code = methodBodyOf(body, method);
|
|
479
525
|
if (!code) continue;
|
|
@@ -661,7 +707,7 @@ export async function checkConventions(appDir) {
|
|
|
661
707
|
// modifiers immediately followed by an identifier inside the
|
|
662
708
|
// constructor's parameter list.
|
|
663
709
|
regex: /constructor[ \t]*\([^)]*\b(?:public|private|protected|readonly)[ \t]+\w+/,
|
|
664
|
-
fix: 'Replace `constructor(public x: number)` with `x: number; constructor(x: number) { this.x = x; }`.
|
|
710
|
+
fix: 'Replace `constructor(public x: number)` with `x: number; constructor(x: number) { this.x = x; }`.',
|
|
665
711
|
},
|
|
666
712
|
{
|
|
667
713
|
name: 'import = require',
|