svelte-5-doctor-core 0.4.2 → 0.4.3
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/.turbo/turbo-build.log +1 -1
- package/dist/run-inspect.js +37 -12
- package/package.json +1 -1
- package/src/run-inspect.ts +31 -12
package/.turbo/turbo-build.log
CHANGED
package/dist/run-inspect.js
CHANGED
|
@@ -30,7 +30,7 @@ const lineColFromIndex = (source, idx) => {
|
|
|
30
30
|
const lines = before.split("\n");
|
|
31
31
|
return { line: lines.length, column: (lines.at(-1)?.length ?? 0) + 1 };
|
|
32
32
|
};
|
|
33
|
-
const runRulesOnFile = (filePath, source) => {
|
|
33
|
+
const runRulesOnFile = (filePath, source, directory) => {
|
|
34
34
|
const diags = [];
|
|
35
35
|
const isSvelte = SVELTE_RE.test(filePath) || filePath.endsWith(".svelte");
|
|
36
36
|
const isRunesFile = /\$state|\$derived|\$effect|\$props|\$bindable|\$inspect/.test(source);
|
|
@@ -379,13 +379,28 @@ const runRulesOnFile = (filePath, source) => {
|
|
|
379
379
|
report("svelte-5-doctor/derived-writable-version-gate", `Derived '${v}' reassigned — needs svelte@5.25+ (check package.json svelte version)`, m.index ?? 0);
|
|
380
380
|
}
|
|
381
381
|
}
|
|
382
|
-
// experimental-async: await in template
|
|
382
|
+
// experimental-async: await in $derived/template needs svelte.config.js experimental.async:true (Svelte 5.36+)
|
|
383
383
|
if (/\bawait\b/.test(source) && (/\$derived.*await/.test(source) || /\{@await/.test(source) || /\{#await/.test(source))) {
|
|
384
|
-
|
|
385
|
-
|
|
384
|
+
let hasFlag = false;
|
|
385
|
+
if (directory) {
|
|
386
|
+
for (const cfg of ["svelte.config.js", "svelte.config.ts"]) {
|
|
387
|
+
try {
|
|
388
|
+
const cfgPath = join(directory, cfg);
|
|
389
|
+
if (existsSync(cfgPath)) {
|
|
390
|
+
const cfgSrc = readFileSync(cfgPath, "utf-8");
|
|
391
|
+
if (/experimental\s*:\s*\{\s*async\s*:\s*true/.test(cfgSrc)) {
|
|
392
|
+
hasFlag = true;
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch { }
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (!hasFlag)
|
|
401
|
+
hasFlag = /experimental\s*:\s*\{\s*async\s*:\s*true/.test(source);
|
|
386
402
|
if (!hasFlag && isSvelte) {
|
|
387
403
|
const idx = source.indexOf("await");
|
|
388
|
-
// only report if file has await in derived/template and not in .js with flag
|
|
389
404
|
report("svelte-5-doctor/experimental-async", "await in $derived/template needs svelte.config.js compilerOptions.experimental.async:true", idx);
|
|
390
405
|
}
|
|
391
406
|
}
|
|
@@ -421,23 +436,33 @@ const runRulesOnFile = (filePath, source) => {
|
|
|
421
436
|
if (idx !== -1)
|
|
422
437
|
report("svelte-5-doctor/props-fallback-no-mutate", `Fallback prop '${fb}' mutated — fallback is not proxied, reassign or make $bindable`, idx);
|
|
423
438
|
}
|
|
424
|
-
// bindable-mutation-without-bindable: prop mutated without $bindable
|
|
439
|
+
// bindable-mutation-without-bindable: prop mutated without $bindable — ignore default value in let {a = default} = $props()
|
|
425
440
|
for (const pv of propsVars) {
|
|
426
441
|
if (bindableVars.has(pv))
|
|
427
442
|
continue;
|
|
443
|
+
// Find props block to exclude default values inside let { ... } = $props()
|
|
444
|
+
const propsBlockMatch = source.match(/let\s*\{[^}]+\}\s*=\s*\$props\(\)/);
|
|
445
|
+
const propsBlockStart = propsBlockMatch?.index ?? -1;
|
|
446
|
+
const propsBlockEnd = propsBlockStart !== -1 ? propsBlockStart + (propsBlockMatch?.[0]?.length ?? 0) : -1;
|
|
428
447
|
const re = new RegExp(`\\b${pv}\\s*\\.\\w+\\s*(=|\\+=|\\+\\+)|\\b${pv}\\s*=\\s*[^=]`);
|
|
429
448
|
const m = re.exec(source);
|
|
430
449
|
if (m) {
|
|
431
450
|
const idx = m.index ?? source.indexOf(pv);
|
|
432
|
-
//
|
|
451
|
+
// Skip if inside props destructuring default value
|
|
452
|
+
if (propsBlockStart !== -1 && idx >= propsBlockStart && idx < propsBlockEnd)
|
|
453
|
+
continue;
|
|
433
454
|
if (source.slice(idx, idx + 50).includes("=") || source.slice(idx, idx + 50).includes("++")) {
|
|
434
|
-
// check if it's inside $props area? crude: if file has $props and pv is prop, and mutation exists
|
|
435
455
|
if (new RegExp(`\\b${pv}\\b.*=.*`).test(source)) {
|
|
436
|
-
// avoid false positive for let {pv} = $props() itself
|
|
437
456
|
const afterProps = source.indexOf("$props()");
|
|
438
457
|
const mutIdx = source.indexOf(pv, afterProps + 10);
|
|
439
|
-
if (mutIdx !== -1 && source.slice(mutIdx, mutIdx + 100).match(/\.|\+|=/)) {
|
|
440
|
-
report("svelte-5-doctor/bindable-mutation-without-bindable", `Prop '${pv}' mutated without $bindable — mark let {${pv}=$bindable()} or use callback prop`,
|
|
458
|
+
if (mutIdx !== -1 && mutIdx !== idx && source.slice(mutIdx, mutIdx + 100).match(/\.|\+|=/)) {
|
|
459
|
+
report("svelte-5-doctor/bindable-mutation-without-bindable", `Prop '${pv}' mutated without $bindable — mark let {${pv}=$bindable()} or use callback prop`, mutIdx);
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
// Also check for mutation after props block, not just first occurrence
|
|
463
|
+
const allMuts = [...source.matchAll(new RegExp(`\\b${pv}\\s*(?:\\.\\w+\\s*=|\\s*=\\s*[^=])`, "g"))].filter((mm) => (mm.index ?? 0) > propsBlockEnd);
|
|
464
|
+
if (allMuts.length > 0) {
|
|
465
|
+
report("svelte-5-doctor/bindable-mutation-without-bindable", `Prop '${pv}' mutated without $bindable — mark let {${pv}=$bindable()} or use callback prop`, allMuts[0].index ?? idx);
|
|
441
466
|
break;
|
|
442
467
|
}
|
|
443
468
|
}
|
|
@@ -742,7 +767,7 @@ export const runInspect = async (input) => {
|
|
|
742
767
|
});
|
|
743
768
|
}
|
|
744
769
|
}
|
|
745
|
-
const heuristic = runRulesOnFile(rel, source);
|
|
770
|
+
const heuristic = runRulesOnFile(rel, source, directory);
|
|
746
771
|
allDiagnostics.push(...heuristic);
|
|
747
772
|
}
|
|
748
773
|
if (input.categories?.length) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-5-doctor-core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Svelte 5 Doctor core — diagnostic engine for Svelte 5. Ported from @react-doctor/core. Renamed from @svelte-doctor/core for npm publish (svelte-doctor taken).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
package/src/run-inspect.ts
CHANGED
|
@@ -36,7 +36,7 @@ const lineColFromIndex = (source: string, idx: number) => {
|
|
|
36
36
|
return { line: lines.length, column: (lines.at(-1)?.length ?? 0) + 1 };
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
const runRulesOnFile = (filePath: string, source: string): Diagnostic[] => {
|
|
39
|
+
const runRulesOnFile = (filePath: string, source: string, directory?: string): Diagnostic[] => {
|
|
40
40
|
const diags: Diagnostic[] = [];
|
|
41
41
|
const isSvelte = SVELTE_RE.test(filePath) || filePath.endsWith(".svelte");
|
|
42
42
|
const isRunesFile = /\$state|\$derived|\$effect|\$props|\$bindable|\$inspect/.test(source);
|
|
@@ -372,13 +372,23 @@ const runRulesOnFile = (filePath: string, source: string): Diagnostic[] => {
|
|
|
372
372
|
report("svelte-5-doctor/derived-writable-version-gate", `Derived '${v}' reassigned — needs svelte@5.25+ (check package.json svelte version)`, m.index ?? 0);
|
|
373
373
|
}
|
|
374
374
|
}
|
|
375
|
-
// experimental-async: await in template
|
|
375
|
+
// experimental-async: await in $derived/template needs svelte.config.js experimental.async:true (Svelte 5.36+)
|
|
376
376
|
if (/\bawait\b/.test(source) && (/\$derived.*await/.test(source) || /\{@await/.test(source) || /\{#await/.test(source))) {
|
|
377
|
-
|
|
378
|
-
|
|
377
|
+
let hasFlag = false;
|
|
378
|
+
if (directory) {
|
|
379
|
+
for (const cfg of ["svelte.config.js", "svelte.config.ts"]) {
|
|
380
|
+
try {
|
|
381
|
+
const cfgPath = join(directory, cfg);
|
|
382
|
+
if (existsSync(cfgPath)) {
|
|
383
|
+
const cfgSrc = readFileSync(cfgPath, "utf-8");
|
|
384
|
+
if (/experimental\s*:\s*\{\s*async\s*:\s*true/.test(cfgSrc)) { hasFlag = true; break; }
|
|
385
|
+
}
|
|
386
|
+
} catch {}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (!hasFlag) hasFlag = /experimental\s*:\s*\{\s*async\s*:\s*true/.test(source);
|
|
379
390
|
if (!hasFlag && isSvelte) {
|
|
380
391
|
const idx = source.indexOf("await");
|
|
381
|
-
// only report if file has await in derived/template and not in .js with flag
|
|
382
392
|
report("svelte-5-doctor/experimental-async", "await in $derived/template needs svelte.config.js compilerOptions.experimental.async:true", idx);
|
|
383
393
|
}
|
|
384
394
|
}
|
|
@@ -412,22 +422,31 @@ const runRulesOnFile = (filePath: string, source: string): Diagnostic[] => {
|
|
|
412
422
|
const idx = source.search(re);
|
|
413
423
|
if (idx !== -1) report("svelte-5-doctor/props-fallback-no-mutate", `Fallback prop '${fb}' mutated — fallback is not proxied, reassign or make $bindable`, idx);
|
|
414
424
|
}
|
|
415
|
-
// bindable-mutation-without-bindable: prop mutated without $bindable
|
|
425
|
+
// bindable-mutation-without-bindable: prop mutated without $bindable — ignore default value in let {a = default} = $props()
|
|
416
426
|
for (const pv of propsVars) {
|
|
417
427
|
if (bindableVars.has(pv)) continue;
|
|
428
|
+
// Find props block to exclude default values inside let { ... } = $props()
|
|
429
|
+
const propsBlockMatch = source.match(/let\s*\{[^}]+\}\s*=\s*\$props\(\)/);
|
|
430
|
+
const propsBlockStart = propsBlockMatch?.index ?? -1;
|
|
431
|
+
const propsBlockEnd = propsBlockStart !== -1 ? propsBlockStart + (propsBlockMatch?.[0]?.length ?? 0) : -1;
|
|
418
432
|
const re = new RegExp(`\\b${pv}\\s*\\.\\w+\\s*(=|\\+=|\\+\\+)|\\b${pv}\\s*=\\s*[^=]`);
|
|
419
433
|
const m = re.exec(source);
|
|
420
434
|
if (m) {
|
|
421
435
|
const idx = m.index ?? source.indexOf(pv);
|
|
422
|
-
//
|
|
436
|
+
// Skip if inside props destructuring default value
|
|
437
|
+
if (propsBlockStart !== -1 && idx >= propsBlockStart && idx < propsBlockEnd) continue;
|
|
423
438
|
if (source.slice(idx, idx + 50).includes("=") || source.slice(idx, idx + 50).includes("++")) {
|
|
424
|
-
// check if it's inside $props area? crude: if file has $props and pv is prop, and mutation exists
|
|
425
439
|
if (new RegExp(`\\b${pv}\\b.*=.*`).test(source)) {
|
|
426
|
-
// avoid false positive for let {pv} = $props() itself
|
|
427
440
|
const afterProps = source.indexOf("$props()");
|
|
428
441
|
const mutIdx = source.indexOf(pv, afterProps + 10);
|
|
429
|
-
if (mutIdx !== -1 && source.slice(mutIdx, mutIdx + 100).match(/\.|\+|=/)) {
|
|
430
|
-
report("svelte-5-doctor/bindable-mutation-without-bindable", `Prop '${pv}' mutated without $bindable — mark let {${pv}=$bindable()} or use callback prop`,
|
|
442
|
+
if (mutIdx !== -1 && mutIdx !== idx && source.slice(mutIdx, mutIdx + 100).match(/\.|\+|=/)) {
|
|
443
|
+
report("svelte-5-doctor/bindable-mutation-without-bindable", `Prop '${pv}' mutated without $bindable — mark let {${pv}=$bindable()} or use callback prop`, mutIdx);
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
// Also check for mutation after props block, not just first occurrence
|
|
447
|
+
const allMuts = [...source.matchAll(new RegExp(`\\b${pv}\\s*(?:\\.\\w+\\s*=|\\s*=\\s*[^=])`, "g"))].filter((mm) => (mm.index ?? 0) > propsBlockEnd);
|
|
448
|
+
if (allMuts.length > 0) {
|
|
449
|
+
report("svelte-5-doctor/bindable-mutation-without-bindable", `Prop '${pv}' mutated without $bindable — mark let {${pv}=$bindable()} or use callback prop`, allMuts[0]!.index ?? idx);
|
|
431
450
|
break;
|
|
432
451
|
}
|
|
433
452
|
}
|
|
@@ -712,7 +731,7 @@ export const runInspect = async (input: InspectInput): Promise<JsonReport> => {
|
|
|
712
731
|
}
|
|
713
732
|
}
|
|
714
733
|
|
|
715
|
-
const heuristic = runRulesOnFile(rel, source);
|
|
734
|
+
const heuristic = runRulesOnFile(rel, source, directory);
|
|
716
735
|
allDiagnostics.push(...heuristic);
|
|
717
736
|
}
|
|
718
737
|
|