@drskillissue/ganko 0.1.22 → 0.1.23

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.
@@ -11732,7 +11732,8 @@ var CONDITIONAL_MOUNT_TAGS = /* @__PURE__ */ new Set([
11732
11732
  ]);
11733
11733
  var messages16 = {
11734
11734
  loadingMismatch: "createResource '{{name}}' has no initialValue but uses manual loading checks ({{name}}.loading). Without initialValue, Suspense intercepts before your loading UI renders. Add initialValue to the options: createResource(fetcher, { initialValue: ... })",
11735
- conditionalSuspense: "createResource '{{name}}' has no initialValue and is rendered inside a conditional mount point ({{mountTag}}). This will trigger a distant Suspense boundary and unmount the entire subtree. Add initialValue to the options: createResource(fetcher, { initialValue: ... })"
11735
+ conditionalSuspense: "createResource '{{name}}' is rendered inside a conditional mount point ({{mountTag}}) with a distant Suspense boundary. When the fetcher's Promise is pending, the SuspenseContext increment fires and unmounts the entire subtree. initialValue does NOT prevent this \u2014 it only prevents the accessor from returning undefined.",
11736
+ missingErrorBoundary: "createResource '{{name}}' has no <ErrorBoundary> between its component and the nearest <Suspense>. When the fetcher throws (network error, 401/403/503, timeout), the error propagates to Suspense which absorbs it and stays in its fallback state permanently. Wrap the component in <ErrorBoundary fallback={...}> or catch errors inside the fetcher."
11736
11737
  };
11737
11738
  var options16 = {};
11738
11739
  function hasInitialValue(call) {
@@ -11775,35 +11776,92 @@ function hasLoadingRead(resourceVariable) {
11775
11776
  }
11776
11777
  return false;
11777
11778
  }
11778
- function findConditionalMountAncestor(graph, componentName) {
11779
+ function resolveFetcherFunction(graph, call) {
11780
+ const args = call.node.arguments;
11781
+ if (args.length === 0) return null;
11782
+ let fetcherNode;
11783
+ if (args.length === 1) {
11784
+ fetcherNode = args[0];
11785
+ } else if (args.length === 2) {
11786
+ const lastArg = args[1];
11787
+ fetcherNode = lastArg && lastArg.type === "ObjectExpression" ? args[0] : args[1];
11788
+ } else {
11789
+ fetcherNode = args[1];
11790
+ }
11791
+ if (!fetcherNode) return null;
11792
+ if (fetcherNode.type === "ArrowFunctionExpression" || fetcherNode.type === "FunctionExpression") {
11793
+ return graph.functionsByNode.get(fetcherNode) ?? null;
11794
+ }
11795
+ if (fetcherNode.type === "Identifier") {
11796
+ const fns = graph.functionsByName.get(fetcherNode.name);
11797
+ if (fns && fns.length > 0) {
11798
+ const fn = fns[0];
11799
+ if (fn) return fn;
11800
+ }
11801
+ }
11802
+ return null;
11803
+ }
11804
+ function fetcherCanThrow(graph, fn, visited) {
11805
+ if (visited.has(fn.id)) return false;
11806
+ visited.add(fn.id);
11807
+ if (fn.async && fn.awaitRanges.length > 0) return true;
11808
+ if (fn.hasThrowStatement) return true;
11809
+ const callSites = fn.callSites;
11810
+ for (let i = 0, len = callSites.length; i < len; i++) {
11811
+ const callSite = callSites[i];
11812
+ if (!callSite) continue;
11813
+ if (!callSite.resolvedTarget) return true;
11814
+ if (fetcherCanThrow(graph, callSite.resolvedTarget, visited)) return true;
11815
+ }
11816
+ return false;
11817
+ }
11818
+ function analyzeComponentBoundaries(graph, componentName) {
11819
+ const result = {
11820
+ conditionalMountTag: null,
11821
+ suspenseDistance: 0,
11822
+ lacksErrorBoundary: false
11823
+ };
11779
11824
  const usages = graph.jsxByTag.get(componentName) ?? [];
11825
+ if (usages.length === 0) return result;
11780
11826
  for (let i = 0, len = usages.length; i < len; i++) {
11781
11827
  const usage = usages[i];
11782
11828
  if (!usage) continue;
11783
11829
  let current = usage.parent;
11784
11830
  let conditionalTag = null;
11785
11831
  let componentLevels = 0;
11832
+ let foundErrorBoundary = false;
11833
+ let foundSuspense = false;
11786
11834
  while (current) {
11787
11835
  const tag = current.tag;
11788
11836
  if (tag && !current.isDomElement) {
11789
11837
  componentLevels++;
11790
- if (tag === "Suspense") {
11838
+ if (tag === "ErrorBoundary") {
11839
+ foundErrorBoundary = true;
11840
+ } else if (tag === "Suspense") {
11841
+ foundSuspense = true;
11842
+ if (!foundErrorBoundary) {
11843
+ result.lacksErrorBoundary = true;
11844
+ }
11791
11845
  if (conditionalTag !== null && componentLevels > 1) {
11792
- return { tag: conditionalTag, suspenseDistance: componentLevels };
11846
+ result.conditionalMountTag = conditionalTag;
11847
+ result.suspenseDistance = componentLevels;
11793
11848
  }
11794
- return null;
11795
- }
11796
- if (conditionalTag === null && CONDITIONAL_MOUNT_TAGS.has(tag)) {
11849
+ break;
11850
+ } else if (conditionalTag === null && CONDITIONAL_MOUNT_TAGS.has(tag)) {
11797
11851
  conditionalTag = tag;
11798
11852
  }
11799
11853
  }
11800
11854
  current = current.parent;
11801
11855
  }
11802
- if (conditionalTag !== null) {
11803
- return { tag: conditionalTag, suspenseDistance: componentLevels };
11856
+ if (!foundSuspense && !foundErrorBoundary) {
11857
+ result.lacksErrorBoundary = true;
11858
+ if (conditionalTag !== null) {
11859
+ result.conditionalMountTag = conditionalTag;
11860
+ result.suspenseDistance = componentLevels;
11861
+ }
11804
11862
  }
11805
11863
  }
11806
- return null;
11864
+ return result;
11807
11865
  }
11808
11866
  function getContainingComponentName(graph, call) {
11809
11867
  const selfComponent = graph.componentScopes.get(call.scope);
@@ -11825,7 +11883,7 @@ var resourceImplicitSuspense = defineSolidRule({
11825
11883
  severity: "warn",
11826
11884
  messages: messages16,
11827
11885
  meta: {
11828
- description: "Detect createResource without initialValue that implicitly triggers Suspense boundaries.",
11886
+ description: "Detect createResource that implicitly triggers or permanently breaks Suspense boundaries.",
11829
11887
  fixable: false,
11830
11888
  category: "reactivity"
11831
11889
  },
@@ -11833,30 +11891,37 @@ var resourceImplicitSuspense = defineSolidRule({
11833
11891
  check(graph, emit) {
11834
11892
  const resourceCalls = getCallsByPrimitive(graph, "createResource");
11835
11893
  if (resourceCalls.length === 0) return;
11894
+ const throwVisited = /* @__PURE__ */ new Set();
11895
+ const boundaryCache = /* @__PURE__ */ new Map();
11836
11896
  for (let i = 0, len = resourceCalls.length; i < len; i++) {
11837
11897
  const call = resourceCalls[i];
11838
11898
  if (!call) continue;
11839
- if (hasInitialValue(call)) continue;
11840
11899
  const resourceName = getResourceVariableName(call);
11841
11900
  if (!resourceName) continue;
11842
- const resourceVariable = findResourceVariable(graph, resourceName);
11843
- if (resourceVariable && hasLoadingRead(resourceVariable)) {
11844
- emit(
11845
- createDiagnostic(
11846
- graph.file,
11847
- call.node,
11848
- "resource-implicit-suspense",
11849
- "loadingMismatch",
11850
- resolveMessage(messages16.loadingMismatch, { name: resourceName }),
11851
- "warn"
11852
- )
11853
- );
11854
- continue;
11855
- }
11901
+ const hasInitial = hasInitialValue(call);
11856
11902
  const componentName = getContainingComponentName(graph, call);
11903
+ if (!hasInitial) {
11904
+ const resourceVariable = findResourceVariable(graph, resourceName);
11905
+ if (resourceVariable && hasLoadingRead(resourceVariable)) {
11906
+ emit(
11907
+ createDiagnostic(
11908
+ graph.file,
11909
+ call.node,
11910
+ "resource-implicit-suspense",
11911
+ "loadingMismatch",
11912
+ resolveMessage(messages16.loadingMismatch, { name: resourceName }),
11913
+ "warn"
11914
+ )
11915
+ );
11916
+ }
11917
+ }
11857
11918
  if (!componentName) continue;
11858
- const conditional = findConditionalMountAncestor(graph, componentName);
11859
- if (conditional) {
11919
+ let analysis = boundaryCache.get(componentName);
11920
+ if (!analysis) {
11921
+ analysis = analyzeComponentBoundaries(graph, componentName);
11922
+ boundaryCache.set(componentName, analysis);
11923
+ }
11924
+ if (analysis.conditionalMountTag) {
11860
11925
  emit(
11861
11926
  createDiagnostic(
11862
11927
  graph.file,
@@ -11865,12 +11930,27 @@ var resourceImplicitSuspense = defineSolidRule({
11865
11930
  "conditionalSuspense",
11866
11931
  resolveMessage(messages16.conditionalSuspense, {
11867
11932
  name: resourceName,
11868
- mountTag: conditional.tag
11933
+ mountTag: analysis.conditionalMountTag
11869
11934
  }),
11870
11935
  "error"
11871
11936
  )
11872
11937
  );
11873
11938
  }
11939
+ if (analysis.lacksErrorBoundary) {
11940
+ const fetcherFn = resolveFetcherFunction(graph, call);
11941
+ if (fetcherFn && fetcherCanThrow(graph, fetcherFn, throwVisited)) {
11942
+ emit(
11943
+ createDiagnostic(
11944
+ graph.file,
11945
+ call.node,
11946
+ "resource-implicit-suspense",
11947
+ "missingErrorBoundary",
11948
+ resolveMessage(messages16.missingErrorBoundary, { name: resourceName }),
11949
+ "error"
11950
+ )
11951
+ );
11952
+ }
11953
+ }
11874
11954
  }
11875
11955
  }
11876
11956
  });