@lobb-js/studio 0.1.33 → 0.1.35

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": "@lobb-js/studio",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -16,11 +16,11 @@
16
16
  loadExtensions,
17
17
  } from "$lib/extensions/extensionUtils";
18
18
  import { mediaQueries } from "$lib/utils";
19
- import Home from "../../routes/home.svelte";
20
- import DataModel from "../../routes/data_model/dataModel.svelte";
21
- import Collections from "../../routes/collections/collections.svelte";
22
- import Workflows from "../../routes/workflows/workflows.svelte";
23
- import Extension from "../../routes/extensions/extension.svelte";
19
+ import Home from "./routes/home.svelte";
20
+ import DataModel from "./routes/data_model/dataModel.svelte";
21
+ import Collections from "./routes/collections/collections.svelte";
22
+ import Workflows from "./routes/workflows/workflows.svelte";
23
+ import Extension from "./routes/extensions/extension.svelte";
24
24
 
25
25
  interface StudioProps {
26
26
  extensions?: any[];
@@ -0,0 +1,46 @@
1
+ <script>
2
+ import { CircleSlash2 } from "lucide-svelte";
3
+ import DataTable from "$lib/components/dataTable/dataTable.svelte";
4
+ import SidebarTrigger from "$lib/components/sidebar/sidebarTrigger.svelte";
5
+ import { ctx } from "$lib/store.svelte";
6
+ import Singletone from "$lib/components/singletone.svelte";
7
+
8
+ let { collectionName } = $props();
9
+ let isSingletonCollection = $derived(ctx.meta.collections[collectionName].singleton);
10
+
11
+ let containerWidth = $state();
12
+ </script>
13
+
14
+ <div bind:clientWidth={containerWidth} class="h-full">
15
+ {#if collectionName}
16
+ {#if isSingletonCollection}
17
+ <Singletone collectionName={collectionName} />
18
+ {:else}
19
+ <DataTable
20
+ {collectionName}
21
+ tableProps={{
22
+ parentWidth: containerWidth,
23
+ }}
24
+ >
25
+ {#snippet headerLeft()}
26
+ <SidebarTrigger />
27
+ {/snippet}
28
+ </DataTable>
29
+ {/if}
30
+ {:else}
31
+ <div
32
+ class="relative flex h-full w-full flex-col items-center justify-center gap-4 text-muted-foreground"
33
+ >
34
+ <CircleSlash2 class="opacity-50" size="50" />
35
+ <div class="flex flex-col items-center justify-center">
36
+ <div>No collection selected</div>
37
+ <div class="text-xs">
38
+ Select a collection to view its entries or create new ones
39
+ </div>
40
+ </div>
41
+ <div class="absolute top-0 left-0 p-2.5">
42
+ <SidebarTrigger />
43
+ </div>
44
+ </div>
45
+ {/if}
46
+ </div>
@@ -0,0 +1,43 @@
1
+ <script lang="ts">
2
+ import type { SideBarData } from "$lib/components/sidebar/sidebarElements.svelte";
3
+ import Sidebar from "$lib/components/sidebar/sidebar.svelte";
4
+ import { ctx } from "$lib/store.svelte";
5
+ import Collection from "./collection.svelte";
6
+ import { Table } from "lucide-svelte";
7
+
8
+ let { collectionName } = $props();
9
+
10
+ const collectionsList = $state(getCollectionsList());
11
+
12
+ function getCollectionsList() {
13
+ const collections = ctx.meta.collections;
14
+ let collectionsOwners: SideBarData = Object.entries(collections).map(
15
+ ([collectionName, collectionValue]) => {
16
+ return {
17
+ name: collectionName,
18
+ path: collectionValue.category ?? collectionValue.owner,
19
+ icon: Table,
20
+ href: `/collections/${collectionName}`,
21
+ };
22
+ },
23
+ );
24
+
25
+ // updating the path from '__project' and '__core' to a more readable names
26
+ collectionsOwners = collectionsOwners.map((item) => {
27
+ if (item.path === "__project") {
28
+ item.path = "project";
29
+ } else if (item.path === "__core") {
30
+ item.path = "core";
31
+ }
32
+ return item;
33
+ });
34
+
35
+ return collectionsOwners;
36
+ }
37
+ </script>
38
+
39
+ <Sidebar title="Collections" data={collectionsList}>
40
+ {#key collectionName}
41
+ <Collection {collectionName} />
42
+ {/key}
43
+ </Sidebar>
@@ -0,0 +1,5 @@
1
+ declare const Collections: import("svelte").Component<{
2
+ collectionName: any;
3
+ }, {}, "">;
4
+ type Collections = ReturnType<typeof Collections>;
5
+ export default Collections;
@@ -0,0 +1,40 @@
1
+ <script lang="ts">
2
+ import { SvelteFlowProvider } from "@xyflow/svelte";
3
+ import Flow from "./flow.svelte";
4
+ import Sidebar from "$lib/components/sidebar/sidebar.svelte";
5
+ import { location } from "@wjfe/n-savant";
6
+ import SyncManager from "./syncManager.svelte";
7
+ import SidebarTrigger from "$lib/components/sidebar/sidebarTrigger.svelte";
8
+
9
+ const currentPage = $derived(location.url.pathname.split("/")[2]);
10
+ </script>
11
+
12
+ <Sidebar
13
+ title="Data Model"
14
+ showSearch={false}
15
+ data={[
16
+ {
17
+ name: "graph",
18
+ href: "/datamodel/graph",
19
+ },
20
+ {
21
+ name: "query_editor",
22
+ href: "/datamodel/query_editor",
23
+ },
24
+ ]}
25
+ >
26
+ <div class="relative h-full w-full">
27
+ {#if currentPage === "graph"}
28
+ <SvelteFlowProvider>
29
+ <div style:width="100%" style:height="100%">
30
+ <Flow />
31
+ </div>
32
+ </SvelteFlowProvider>
33
+ {:else if currentPage === "query_editor"}
34
+ <SyncManager />
35
+ {/if}
36
+ <div class="absolute top-0 left-0 p-2.5">
37
+ <SidebarTrigger />
38
+ </div>
39
+ </div>
40
+ </Sidebar>
@@ -0,0 +1,3 @@
1
+ declare const DataModel: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type DataModel = ReturnType<typeof DataModel>;
3
+ export default DataModel;
@@ -0,0 +1,22 @@
1
+ .svelte-flow {
2
+ --xy-edge-stroke-default: hsl(var(--muted-foreground));
3
+ --xy-edge-stroke-selected-default: hsl(var(--primary));
4
+ --xy-connectionline-stroke-default: hsl(var(--primary));
5
+ --xy-attribution-background-color-default: transparent;
6
+ --xy-minimap-background-color-default: hsl(var(--background));
7
+ --xy-minimap-mask-background-color-default: hsl(var(--primary) / 0.1);
8
+ --xy-minimap-node-background-color-default: hsl(var(--primary) / 0.2);
9
+ --xy-background-color-default: hsl(var(--soft));
10
+ --xy-background-pattern-dots-color-default: hsl(var(--muted-foreground));
11
+ --xy-background-pattern-lines-color-default: hsl(var(--background));
12
+ --xy-background-pattern-cross-color-default: hsl(var(--soft));
13
+ --xy-node-border-default: 1px solid hsl(var(--primary) / 0.25);
14
+ --xy-node-background-color-default: hsl(var(--background));
15
+ --xy-node-boxshadow-selected-default: 0 0 0 0.5px hsl(var(--primary) / 0.5);
16
+ --xy-handle-background-color-default: hsl(var(--primary));
17
+ --xy-handle-border-color-default: hsl(var(--background));
18
+ --xy-controls-button-background-color-default: hsl(var(--background));
19
+ --xy-controls-button-background-color-hover-default: hsl(var(--muted));
20
+ --xy-controls-button-border-color-default: hsl(var(--muted));
21
+ --xy-edge-label-background-color-default: hsl(var(--background));
22
+ }
@@ -0,0 +1,82 @@
1
+ <script lang="ts">
2
+ import "@xyflow/svelte/dist/style.css";
3
+ import "./flow.css";
4
+
5
+ import type { Node, Edge } from "@xyflow/svelte";
6
+
7
+ import {
8
+ SvelteFlow,
9
+ Background,
10
+ MiniMap,
11
+ Controls,
12
+ useSvelteFlow,
13
+ } from "@xyflow/svelte";
14
+ import { getLayoutedElements } from "./utils";
15
+ import { onMount } from "svelte";
16
+ import { ctx } from "$lib/store.svelte";
17
+
18
+ const { fitView } = useSvelteFlow();
19
+
20
+ let nodes = $state.raw<Node[]>(generateNodes());
21
+ let edges = $state.raw<Edge[]>(generateEdges());
22
+
23
+ onMount(() => {
24
+ setTimeout(() => {
25
+ onLayout();
26
+ }, 0);
27
+ });
28
+
29
+ function generateNodes() {
30
+ const localNodes: Node[] = [];
31
+
32
+ for (const [collectionName, collectionValue] of Object.entries(
33
+ ctx.meta.collections,
34
+ )) {
35
+ if (collectionValue.owner !== "__project") {
36
+ continue;
37
+ }
38
+
39
+ localNodes.push({
40
+ id: collectionName,
41
+ data: { label: collectionName },
42
+ position: { x: 0, y: 0 },
43
+ });
44
+ }
45
+
46
+ return localNodes;
47
+ }
48
+
49
+ function generateEdges() {
50
+ const localEdges: Edge[] = [];
51
+
52
+ const relations = ctx.meta.relations;
53
+ for (let index = 0; index < relations.length; index++) {
54
+ const relation = relations[index];
55
+ localEdges.push({
56
+ id: `${relation.from.collection}_${relation.to.collection}`,
57
+ source: relation.from.collection,
58
+ target: relation.to.collection,
59
+ animated: true,
60
+ });
61
+ }
62
+
63
+ return localEdges;
64
+ }
65
+
66
+ function onLayout() {
67
+ const layouted = getLayoutedElements(nodes, edges);
68
+
69
+ nodes = [...layouted.nodes];
70
+ edges = [...layouted.edges];
71
+
72
+ fitView({
73
+ padding: 0.5,
74
+ });
75
+ }
76
+ </script>
77
+
78
+ <SvelteFlow bind:nodes bind:edges>
79
+ <Background />
80
+ <MiniMap />
81
+ <Controls />
82
+ </SvelteFlow>
@@ -0,0 +1,5 @@
1
+ import "@xyflow/svelte/dist/style.css";
2
+ import "./flow.css";
3
+ declare const Flow: import("svelte").Component<Record<string, never>, {}, "">;
4
+ type Flow = ReturnType<typeof Flow>;
5
+ export default Flow;
@@ -0,0 +1,93 @@
1
+ <script lang="ts">
2
+ import DiffViewer from "$lib/components/diffViewer.svelte";
3
+ import { ctx } from "$lib/store.svelte";
4
+ import { onMount } from "svelte";
5
+ import stringify from "json-stable-stringify";
6
+ import MonacoEditor from "$lib/components/monacoEditor.svelte";
7
+ import Table from "$lib/components/dataTable/table.svelte";
8
+ import Button from "$lib/components/ui/button/button.svelte";
9
+ import { LoaderCircle, SendHorizontal } from "lucide-svelte";
10
+ import { lobb } from "$lib";
11
+
12
+ let configSchema: string = $state("");
13
+ let dbSchema: string = $state("");
14
+ let sqlPrompt = $state("");
15
+ let sqlResult = $state([]);
16
+
17
+ onMount(() => {
18
+ loadSchemas();
19
+ });
20
+
21
+ async function loadSchemas() {
22
+ configSchema = "";
23
+ dbSchema = "";
24
+ const response = await fetch(`${ctx.lobbUrl}/api/schema/diff`);
25
+ const result = await response.json();
26
+ configSchema = stringify(result.dbSchema, {
27
+ space: 2,
28
+ }) as string;
29
+ dbSchema = stringify(result.configSchema, {
30
+ space: 2,
31
+ }) as string;
32
+ }
33
+
34
+ async function handleExecute() {
35
+ const response = await lobb.createOne("core_query", {
36
+ query: sqlPrompt,
37
+ });
38
+ const result = await response.json();
39
+ sqlResult = result.data;
40
+ loadSchemas();
41
+ }
42
+ </script>
43
+
44
+ <div class="h-[50%] border-b">
45
+ {#if configSchema && dbSchema}
46
+ <DiffViewer
47
+ type="json"
48
+ original={configSchema}
49
+ modified={dbSchema}
50
+ class="h-full rounded-none border-0"
51
+ />
52
+ {:else}
53
+ <div class="flex justify-center items-center h-full gap-2">
54
+ <LoaderCircle class="animate-spin" />
55
+ <div>loading...</div>
56
+ </div>
57
+ {/if}
58
+ </div>
59
+ <div class="flex h-[50%] w-full">
60
+ <div class="h-full flex-1 flex flex-col border-r">
61
+ <div
62
+ class="h-10 flex items-center px-2 bg-background border-b justify-between"
63
+ >
64
+ <div>Query Editor</div>
65
+ <Button
66
+ class="h-7 px-3 text-xs font-normal"
67
+ Icon={SendHorizontal}
68
+ onclick={handleExecute}
69
+ >
70
+ Execute
71
+ </Button>
72
+ </div>
73
+ <MonacoEditor
74
+ type="sql"
75
+ name="prompt"
76
+ bind:value={sqlPrompt}
77
+ class="flex-1 rounded-none border-0"
78
+ />
79
+ </div>
80
+ <div class="flex-1">
81
+ {#if Array.isArray(sqlResult) && sqlResult.length}
82
+ <Table
83
+ data={sqlResult}
84
+ showLastRowBorder={true}
85
+ showLastColumnBorder={true}
86
+ />
87
+ {:else}
88
+ <div class="flex flex-1 h-full items-center justify-center">
89
+ No results
90
+ </div>
91
+ {/if}
92
+ </div>
93
+ </div>
@@ -0,0 +1,3 @@
1
+ declare const SyncManager: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type SyncManager = ReturnType<typeof SyncManager>;
3
+ export default SyncManager;
@@ -0,0 +1,4 @@
1
+ export declare function getLayoutedElements(nodes: any[], edges: any[]): {
2
+ nodes: any[];
3
+ edges: any[];
4
+ };
@@ -0,0 +1,35 @@
1
+ import Dagre from '@dagrejs/dagre';
2
+
3
+ export function getLayoutedElements(nodes: any[], edges: any[]) {
4
+ const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
5
+ g.setGraph({ rankdir: "LR" });
6
+
7
+ edges.forEach((edge) => g.setEdge(edge.source, edge.target));
8
+ nodes.forEach((node) => {
9
+ return g.setNode(node.id, {
10
+ ...node,
11
+ width: node.measured?.width ?? 0,
12
+ height: node.measured?.height ?? 0,
13
+ });
14
+ });
15
+
16
+ Dagre.layout(g);
17
+
18
+ return {
19
+ nodes: nodes.map((node) => {
20
+ const position = g.node(node.id);
21
+ // We are shifting the dagre node position (anchor=center center) to the top left
22
+ // so it matches the Svelte Flow node anchor point (top left).
23
+ const x = position.x - (node.measured?.width ?? 0) / 2;
24
+ const y = position.y - (node.measured?.height ?? 0) / 2;
25
+
26
+ return {
27
+ ...node,
28
+ position: { x, y },
29
+ targetPosition: "left",
30
+ sourcePosition: "right",
31
+ };
32
+ }),
33
+ edges,
34
+ };
35
+ }
@@ -0,0 +1,16 @@
1
+ <script>
2
+ import ExtensionsComponents from "$lib/components/extensionsComponents.svelte";
3
+ import { getExtensionUtils } from "$lib/extensions/extensionUtils";
4
+
5
+ let { extension, page } = $props();
6
+ </script>
7
+
8
+ <div class="grid overflow-auto bg-background">
9
+ {#key extension && page}
10
+ <ExtensionsComponents
11
+ name="pages.{page}"
12
+ utils={getExtensionUtils()}
13
+ filterByExtensions={[extension]}
14
+ />
15
+ {/key}
16
+ </div>
@@ -0,0 +1,36 @@
1
+ <script>
2
+ import Button from "$lib/components/ui/button/button.svelte";
3
+ import { ctx } from "$lib/store.svelte";
4
+ import { location } from "@wjfe/n-savant";
5
+ import { ArrowRight } from "lucide-svelte";
6
+ </script>
7
+
8
+ <div class="flex flex-col">
9
+ <div
10
+ class="flex flex-1 w-full flex-col items-center justify-center gap-4 text-muted-foreground"
11
+ >
12
+ <div class="flex flex-col items-center justify-center p-4">
13
+ <div class="text-3xl">Welcome to Lobb!</div>
14
+ <div class="text-xs text-center">
15
+ Your journey starts here. Explore and make the most of your
16
+ experience.
17
+ </div>
18
+ </div>
19
+ <div class="flex flex-col items-center justify-center">
20
+ <Button
21
+ Icon={ArrowRight}
22
+ variant="outline"
23
+ class="h-7 px-3 text-xs font-normal"
24
+ onclick={() => location.navigate("/collections")}
25
+ >
26
+ Go to collections
27
+ </Button>
28
+ </div>
29
+ </div>
30
+ <div class="flex justify-end p-2 text-xs text-muted-foreground/50">
31
+ <div class="flex flex-col text-end">
32
+ <div>studio: v{ctx.studioVersion}</div>
33
+ <div>core: v{ctx.meta.version}</div>
34
+ </div>
35
+ </div>
36
+ </div>
@@ -0,0 +1,135 @@
1
+ <script lang="ts">
2
+ import type { SideBarData } from "$lib/components/sidebar/sidebarElements.svelte";
3
+ import WorkflowEditor, {
4
+ type WorkflowEntry,
5
+ } from "$lib/components/workflowEditor.svelte";
6
+ import { lobb } from "$lib";
7
+ import Sidebar from "$lib/components/sidebar/sidebar.svelte";
8
+ import Button from "$lib/components/ui/button/button.svelte";
9
+ import { ctx } from "$lib/store.svelte";
10
+ import { location } from "@wjfe/n-savant";
11
+ import { CircleSlash2, Plus, Trash2 } from "lucide-svelte";
12
+ import { onMount } from "svelte";
13
+ import { showDialog } from "$lib/components/confirmationDialog/store.svelte";
14
+ import SidebarTrigger from "$lib/components/sidebar/sidebarTrigger.svelte";
15
+
16
+ let { workflowName } = $props();
17
+
18
+ let sidebarData: SideBarData | null = $state(null);
19
+ let workflowEntry: WorkflowEntry | null = $state(null);
20
+
21
+ onMount(async () => {
22
+ getSidebarData();
23
+ });
24
+
25
+ $effect(() => {
26
+ fetchWorkflowData(workflowName);
27
+ });
28
+
29
+ async function getSidebarData() {
30
+ const response = await lobb.findAll("core_workflows", {});
31
+ const result = await response.json();
32
+ const workflows: any[] = result.data;
33
+ sidebarData = workflows.map((workflow) => {
34
+ return {
35
+ name: workflow.name,
36
+ path: workflow.directory,
37
+ onclick: () => {
38
+ location.navigate(`/workflows/${workflow.name}`);
39
+ },
40
+ meta: {
41
+ id: workflow.id,
42
+ },
43
+ };
44
+ });
45
+ }
46
+
47
+ async function fetchWorkflowData(workflowName: string) {
48
+ if (workflowName && workflowName !== "new") {
49
+ const response = await lobb.findAll("core_workflows", {
50
+ filter: {
51
+ name: workflowName,
52
+ },
53
+ });
54
+ const result = await response.json();
55
+ const workflow = result.data[0];
56
+ workflowEntry = workflow;
57
+ } else {
58
+ const workflowHandlerDefaultValue =
59
+ ctx.meta.collections.core_workflows.fields.handler
60
+ .pre_processors.default;
61
+ workflowEntry = {
62
+ name: "",
63
+ event_name: "",
64
+ handler: workflowHandlerDefaultValue,
65
+ directory: "",
66
+ };
67
+ }
68
+ }
69
+
70
+ async function handleWorkflowDelete(
71
+ workflowName: string,
72
+ workflowId: string,
73
+ ) {
74
+ const result = await showDialog(
75
+ "Are you sure?",
76
+ "This will delete the Workflow you selected.",
77
+ );
78
+ if (result) {
79
+ await lobb.deleteOne("core_workflows", workflowId);
80
+ getSidebarData();
81
+ if (workflowEntry && workflowName === workflowEntry.name) {
82
+ location.navigate("/workflows");
83
+ }
84
+ }
85
+ }
86
+ </script>
87
+
88
+ <Sidebar title="Workflows" data={sidebarData}>
89
+ {#snippet belowSearch()}
90
+ <div class="pb-4 px-2">
91
+ <Button
92
+ class="h-7 px-3 text-xs font-normal w-full"
93
+ variant="outline"
94
+ onclick={() => location.navigate("/workflows/new")}
95
+ Icon={Plus}
96
+ >
97
+ Create a Workflow
98
+ </Button>
99
+ </div>
100
+ {/snippet}
101
+ {#snippet elementRightSide(element)}
102
+ <Button
103
+ class="h-6 w-6 text-muted-foreground hover:bg-transparent"
104
+ variant="ghost"
105
+ size="icon"
106
+ onclick={() => handleWorkflowDelete(element.name, element.meta?.id)}
107
+ Icon={Trash2}
108
+ ></Button>
109
+ {/snippet}
110
+ <div class="relative h-full w-full">
111
+ {#if workflowName === undefined}
112
+ <div
113
+ class="flex h-full w-full flex-col items-center justify-center gap-4 text-muted-foreground"
114
+ >
115
+ <CircleSlash2 class="opacity-50" size="50" />
116
+ <div class="flex flex-col items-center justify-center">
117
+ <div>No workflow selected</div>
118
+ <div class="text-xs">
119
+ Select a workflow to edit it or create new ones
120
+ </div>
121
+ </div>
122
+ </div>
123
+ {:else if workflowEntry}
124
+ {#key workflowEntry}
125
+ <WorkflowEditor
126
+ bind:workflow={workflowEntry}
127
+ refreshSidebar={getSidebarData}
128
+ />
129
+ {/key}
130
+ {/if}
131
+ <div class="absolute top-0 left-0 p-2.5">
132
+ <SidebarTrigger />
133
+ </div>
134
+ </div>
135
+ </Sidebar>
@@ -0,0 +1,5 @@
1
+ declare const Workflows: import("svelte").Component<{
2
+ workflowName: any;
3
+ }, {}, "">;
4
+ type Workflows = ReturnType<typeof Workflows>;
5
+ export default Workflows;
@@ -9,7 +9,8 @@ const __dirname = path.dirname(__filename);
9
9
  * Vite plugin that resolves $lib imports contextually based on the importing file location.
10
10
  *
11
11
  * - Files inside the consuming project -> resolve to local ./src/lib
12
- * - Files outside the project (e.g., Studio package) -> resolve to Studio's src/lib
12
+ * - Files from Studio package in monorepo -> resolve to Studio's src/lib
13
+ * - Files from Studio package in node_modules -> resolve to node_modules/@lobb-js/studio/src/lib
13
14
  *
14
15
  * This allows shadcn components to work in both the Studio package and consuming projects
15
16
  * without conflicts.
@@ -42,8 +43,12 @@ export function contextualLibAlias() {
42
43
  if (isLocalImport && fs.existsSync(localLibPath)) {
43
44
  // Local file importing $lib -> resolve to local src/lib
44
45
  targetPath = localLibPath;
46
+ } else if (importer.includes("node_modules/@lobb-js/studio")) {
47
+ // Import from Studio package in node_modules -> resolve to node_modules/@lobb-js/studio/src/lib
48
+ const nodeModulesStudioPath = importer.substring(0, importer.indexOf("node_modules/@lobb-js/studio") + "node_modules/@lobb-js/studio".length);
49
+ targetPath = path.join(nodeModulesStudioPath, "src/lib");
45
50
  } else if (fs.existsSync(studioLibPath)) {
46
- // External file (e.g., Studio) importing $lib -> resolve to Studio's src/lib
51
+ // External file (e.g., Studio in monorepo) importing $lib -> resolve to Studio's src/lib
47
52
  targetPath = studioLibPath;
48
53
  } else {
49
54
  // Neither path exists, let default resolution handle it