Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,41 @@ Here a some common patterns for different project types:
| Laravel + Vue | `resources/js/Pages/**/*.vue` |
| Laravel + React | `resources/js/Pages/**/*.tsx` |

### `inertia.pageResolvers` (New Feature)

For modular applications (e.g., using Laravel Modules), you can configure multiple page resolvers with prefixes. Each resolver maps a prefix to a specific glob pattern, allowing you to organize pages across different modules.

```json
{
"inertia.pageResolvers": [
{
"prefix": "Shipping",
"pattern": "Modules/Shipping/Pages/**/*.tsx"
},
{
"prefix": "Accounting",
"pattern": "Modules/Accounting/Pages/**/*.tsx"
}
]
}
```

With this configuration, you can reference components using prefixes in your controllers:

```php
// In your controller
return inertia('Shipping:Orders/Index'); // Points to Modules/Shipping/Pages/Orders/Index.tsx
return inertia('Accounting:Reports/Balance'); // Points to Modules/Accounting/Pages/Reports/Balance.tsx
```

**Key benefits:**
- **Modular organization**: Each module has its own page directory
- **Prefix-based resolution**: Use module prefixes to avoid naming conflicts
- **Autocompletion support**: Get intelligent suggestions for all modules
- **Hyperlink navigation**: Click to navigate directly to module pages

**Note**: When using `pageResolvers`, the traditional `pages` setting is optional and serves as a fallback for non-prefixed components.

### `inertia.pathSeparators`

Inertia.js commonly uses a forward slash when it comes to component names
Expand Down
18 changes: 18 additions & 0 deletions example-settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"inertia.pageResolvers": [
{
"prefix": "Shipping",
"pattern": "Modules/Shipping/Pages/**/*.tsx"
},
{
"prefix": "Accounting",
"pattern": "Modules/Accounting/Pages/**/*.tsx"
},
{
"prefix": "Inventory",
"pattern": "Modules/Inventory/Pages/**/*.tsx"
}
],
"inertia.defaultExtension": ".tsx",
"inertia.pathSeparators": ["/", "."]
}
19 changes: 19 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,25 @@
"default": "resources/js/Pages/**/*.vue",
"description": "A glob pattern to match the Vue components that you use as pages in your Inertia application. The root folder of your components is inferred from the glob pattern."
},
"inertia.pageResolvers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"prefix": {
"type": "string",
"description": "The prefix used in component names (e.g., 'Shipping', 'Accounting')"
},
"pattern": {
"type": "string",
"description": "The glob pattern to match components for this resolver (e.g., 'Modules/Shipping/Pages/**/*.tsx')"
}
},
"required": ["prefix", "pattern"]
},
"default": [],
"description": "Array of page resolvers, each with a prefix and corresponding glob pattern. This allows support for modular applications with multiple page directories."
},
"inertia.pathSeparators": {
"type": "array",
"items": {
Expand Down
85 changes: 55 additions & 30 deletions src/editor/autocompletion.provider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { pathDiff, unglob } from "@/helpers";
import {
type PageResolver,
getAllPagePatterns,
pathDiff,
unglob,
} from "@/helpers";
import {
CompletionItem,
CompletionItemKind,
Expand Down Expand Up @@ -38,38 +43,58 @@ export class AutocompletionProvider implements CompletionItemProvider {
return [];
}

const pagesGlob: string | undefined = workspace
.getConfiguration("inertia")
.get("pages");

const config = workspace.getConfiguration("inertia");
const firstPathSeparator: string | undefined =
workspace.getConfiguration("inertia").get("pathSeparators", ["/"])?.[0] ??
"/";
config.get("pathSeparators", ["/"])?.[0] ?? "/";

if (!pagesGlob) {
return undefined;
// Get all page patterns (both legacy and new resolvers)
const pagePatterns = getAllPagePatterns(workspace);

if (pagePatterns.length === 0) {
// Fall back to legacy single pattern if no patterns found
const pagesGlob: string | undefined = config.get("pages");
if (!pagesGlob) {
return undefined;
}
pagePatterns.push({ pattern: pagesGlob });
}

return workspace
.findFiles({
base: workspaceURI.toString(),
baseUri: workspaceURI,
pattern: pagesGlob,
})
.then((files) => {
console.log(files);
return files.map((uri) => {
const base = Uri.joinPath(workspaceURI, unglob(pagesGlob));
return new CompletionItem(
{
label: pathDiff(base, uri)
.replace(/\.[^/.]+$/, "")
.replaceAll("/", firstPathSeparator),
description: "Inertia.js",
},
CompletionItemKind.Value,
);
});
});
// Create completion items for all patterns
const completionPromises = pagePatterns.map(({ pattern, prefix }) =>
workspace
.findFiles({
base: workspaceURI.toString(),
baseUri: workspaceURI,
pattern: pattern,
})
.then((files: Uri[]) => {
return files.map((uri) => {
const base = Uri.joinPath(workspaceURI, unglob(pattern));
const componentPath = pathDiff(base, uri)
.replace(/\.[^/.]+$/, "")
.replaceAll("/", firstPathSeparator);

// Add prefix if this pattern has one
const finalPath = prefix
? `${prefix}:${componentPath}`
: componentPath;

return new CompletionItem(
{
label: finalPath,
description: prefix
? `Inertia.js (${prefix} Module)`
: "Inertia.js",
},
CompletionItemKind.Value,
);
});
}),
);

return Promise.all(completionPromises).then((completionArrays) => {
// Flatten the arrays of completion items
return completionArrays.flat();
});
}
}
75 changes: 58 additions & 17 deletions src/editor/component-link.provider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { locateInDocument, unglob } from "@/helpers";
import {
type PageResolver,
getAllPagePatterns,
locateInDocument,
resolveComponentWithPrefix,
unglob,
} from "@/helpers";
import {
type DocumentLink,
type DocumentLinkProvider,
Expand Down Expand Up @@ -72,14 +78,53 @@ export class ComponentLinkProvider implements DocumentLinkProvider {
return undefined;
}

const pages: string | undefined = workspace
.getConfiguration("inertia")
.get("pages");
const componentName = document.getText(link.range);
const config = workspace.getConfiguration("inertia");

// Handle deprecated setting
const pagesFolder: string | undefined = workspace
.getConfiguration("inertia")
.get("pagesFolder");
// Get page resolvers for prefix-based resolution
const pageResolvers: PageResolver[] = config.get("pageResolvers", []);
const prefixResolution = resolveComponentWithPrefix(
componentName,
pageResolvers,
);

if (prefixResolution) {
// Handle prefix-based component resolution
const { resolver, componentPath } = prefixResolution;

return workspace
.findFiles({
base: workspaceURI.toString(),
baseUri: workspaceURI,
pattern: resolver.pattern,
})
.then((files: Uri[]) => {
const normalizedPath = this.normalizeComponentPath(componentPath);
const file = files.find((file: Uri) => {
return file.path.startsWith(
Uri.joinPath(
workspaceURI,
unglob(resolver.pattern),
normalizedPath,
).path,
);
});

link.target =
file ??
Uri.joinPath(
workspaceURI,
unglob(resolver.pattern),
normalizedPath + config.get("defaultExtension", ".vue"),
);

return link;
});
}

// Fall back to legacy single pattern resolution
const pages: string | undefined = config.get("pages");
const pagesFolder: string | undefined = config.get("pagesFolder");

if (pages === undefined || pagesFolder === undefined) {
return undefined;
Expand All @@ -92,11 +137,9 @@ export class ComponentLinkProvider implements DocumentLinkProvider {
baseUri: workspaceURI,
pattern: pages,
})
.then((files) => {
const path = document.getText(link.range);

.then((files: Uri[]) => {
const file = files.find((file: Uri) => {
const normalized = this.normalizeComponentPath(path);
const normalized = this.normalizeComponentPath(componentName);
return file.path.startsWith(
Uri.joinPath(workspaceURI, unglob(pages), normalized).path,
);
Expand All @@ -107,10 +150,8 @@ export class ComponentLinkProvider implements DocumentLinkProvider {
Uri.joinPath(
workspaceURI,
unglob(pages),
this.normalizeComponentPath(path) +
workspace
.getConfiguration("inertia")
.get("defaultExtension", ".vue"),
this.normalizeComponentPath(componentName) +
config.get("defaultExtension", ".vue"),
);

return link;
Expand All @@ -123,7 +164,7 @@ export class ComponentLinkProvider implements DocumentLinkProvider {
.get("pathSeparators", ["/"]);

return component.replaceAll(
new RegExp(`[${pathSeparators.join("")}]`, "g"),
new RegExp(`[${(pathSeparators || ["/"]).join("")}]`, "g"),
"/",
);
}
Expand Down
58 changes: 58 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { Range, type TextDocument, type Uri } from "vscode";

/**
* Interface for page resolver configuration
*/
export interface PageResolver {
prefix: string;
pattern: string;
}

/**
* Computes the difference between two paths
*/
Expand Down Expand Up @@ -58,3 +66,53 @@ export const locateInDocument = (

return results;
};

/**
* Resolves a component name with prefix to its actual file path
*/
export const resolveComponentWithPrefix = (
componentName: string,
pageResolvers: PageResolver[],
): { resolver: PageResolver; componentPath: string } | null => {
// Check if component has a prefix (format: "Prefix:ComponentPath")
const prefixMatch = componentName.match(/^([^:]+):(.+)$/);

if (!prefixMatch) {
return null;
}

const [, prefix, componentPath] = prefixMatch;
const resolver = pageResolvers.find((r) => r.prefix === prefix);

if (!resolver) {
return null;
}

return { resolver, componentPath };
};

/**
* Gets all configured page patterns (both legacy and new resolver-based)
*/
export const getAllPagePatterns = (workspace: {
getConfiguration: (section: string) => {
get: (key: string, defaultValue?: unknown) => unknown;
};
}): { pattern: string; prefix?: string }[] => {
const config = workspace.getConfiguration("inertia");
const patterns: { pattern: string; prefix?: string }[] = [];

// Add legacy pages pattern if configured
const legacyPages = config.get("pages") as string | undefined;
if (legacyPages) {
patterns.push({ pattern: legacyPages });
}

// Add new page resolvers
const pageResolvers = config.get("pageResolvers", []) as PageResolver[];
for (const resolver of pageResolvers) {
patterns.push({ pattern: resolver.pattern, prefix: resolver.prefix });
}

return patterns;
};
18 changes: 18 additions & 0 deletions test/fixtures/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"inertia.pageResolvers": [
{
"prefix": "Shipping",
"pattern": "Modules/Shipping/Pages/**/*.tsx"
},
{
"prefix": "Accounting",
"pattern": "Modules/Accounting/Pages/**/*.tsx"
},
{
"prefix": "Inventory",
"pattern": "Modules/Inventory/Pages/**/*.tsx"
}
],
"inertia.defaultExtension": ".tsx",
"inertia.pathSeparators": ["/", "."]
}
14 changes: 14 additions & 0 deletions test/fixtures/Modules/Accounting/Pages/Reports/Balance.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Head } from "@inertiajs/react";
import React from "react";

export default function Balance() {
return (
<>
<Head title="Accounting Balance Report" />
<div>
<h1>Balance Report</h1>
<p>Accounting balance sheet report.</p>
</div>
</>
);
}
Empty file added test/fixtures/Modules/Help.tsx
Empty file.
Loading