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.
@@ -1,4 +1,4 @@
1
1
 
2
- > svelte-5-doctor-core@0.4.2 build C:\Users\admin\Documents\Default Project\svelte-doctor\packages\core
2
+ > svelte-5-doctor-core@0.4.3 build C:\Users\admin\Documents\Default Project\svelte-doctor\packages\core
3
3
  > tsc -p tsconfig.json
4
4
 
@@ -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 or $derived without flag
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
- const hasFlag = /experimental\s*:\s*\{\s*async\s*:\s*true/.test(source) || source.includes("experimental.async");
385
- // check svelte.config.js would have flag, but we heuristic: if await in .svelte and no flag in source, report
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
- // ensure it's mutation not just read
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`, idx);
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.2",
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",
@@ -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 or $derived without flag
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
- const hasFlag = /experimental\s*:\s*\{\s*async\s*:\s*true/.test(source) || source.includes("experimental.async");
378
- // check svelte.config.js would have flag, but we heuristic: if await in .svelte and no flag in source, report
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
- // ensure it's mutation not just read
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`, idx);
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