@nifrajs/ts-plugin 2.4.0

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/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @nifrajs/ts-plugin
2
+
3
+ A TypeScript language-service plugin for [nifra](https://github.com/nifrajs/nifra). It adds go-to-definition on route-path string literals: put your cursor on `"/orders"` in `navigate({ to: "/orders" })`, `<Link to="/orders">`, or `href="/orders"`, and jump straight to the `routes/` file that serves it.
4
+
5
+ The routing rules are nifra's own - it discovers routes with `@nifrajs/web` and matches them with `@nifrajs/core`'s pattern matcher - so a path resolves to exactly the file it would serve at runtime, dynamic segments included (`/users/42` → `routes/users/[id].tsx`).
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install -D @nifrajs/ts-plugin
11
+ ```
12
+
13
+ ## Enable
14
+
15
+ Add it to your `tsconfig.json`:
16
+
17
+ ```json
18
+ {
19
+ "compilerOptions": {
20
+ "plugins": [{ "name": "@nifrajs/ts-plugin" }]
21
+ }
22
+ }
23
+ ```
24
+
25
+ In VS Code, run "TypeScript: Select TypeScript Version" from the Command Palette and choose "Use Workspace Version" so the editor loads the plugin from your project.
26
+
27
+ ## For agents
28
+
29
+ Start with [`LLM.md`](./LLM.md) - this package's contract card (the exports it ships and how it wires in). [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus for the whole framework.
30
+
31
+ ## License
32
+
33
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,159 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ function __accessProp(key) {
6
+ return this[key];
7
+ }
8
+ var __toCommonJS = (from) => {
9
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
10
+ if (entry)
11
+ return entry;
12
+ entry = __defProp({}, "__esModule", { value: true });
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (var key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(entry, key))
16
+ __defProp(entry, key, {
17
+ get: __accessProp.bind(from, key),
18
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
19
+ });
20
+ }
21
+ __moduleCache.set(from, entry);
22
+ return entry;
23
+ };
24
+ var __moduleCache;
25
+ var __returnValue = (v) => v;
26
+ function __exportSetter(name, newValue) {
27
+ this[name] = __returnValue.bind(null, newValue);
28
+ }
29
+ var __export = (target, all) => {
30
+ for (var name in all)
31
+ __defProp(target, name, {
32
+ get: all[name],
33
+ enumerable: true,
34
+ configurable: true,
35
+ set: __exportSetter.bind(all, name)
36
+ });
37
+ };
38
+
39
+ // src/index.ts
40
+ var exports_src = {};
41
+ __export(exports_src, {
42
+ findRoutesDir: () => findRoutesDir,
43
+ findRoutePathLiteral: () => findRoutePathLiteral,
44
+ default: () => src_default
45
+ });
46
+ module.exports = __toCommonJS(exports_src);
47
+ var import_node_fs = require("node:fs");
48
+ var import_node_path = require("node:path");
49
+ var import_fs = require("@nifrajs/web/fs");
50
+
51
+ // src/resolve.ts
52
+ var import_pattern = require("@nifrajs/core/pattern");
53
+ var compiledRouteCache = new WeakMap;
54
+ function compiledRoutes(routes) {
55
+ const cached = compiledRouteCache.get(routes);
56
+ if (cached !== undefined)
57
+ return cached;
58
+ const compiled = import_pattern.sortRoutesBySpecificity(routes.map((route) => ({ route, pattern: import_pattern.compileRoutePattern(route.pattern) })));
59
+ compiledRouteCache.set(routes, compiled);
60
+ return compiled;
61
+ }
62
+ function resolveRouteFile(path, routes) {
63
+ if (!path.startsWith("/"))
64
+ return;
65
+ const pathname = path.split(/[?#]/, 1)[0] ?? path;
66
+ for (const { route, pattern } of compiledRoutes(routes)) {
67
+ if (import_pattern.matchRoutePattern(pattern, pathname).matched)
68
+ return route.file;
69
+ }
70
+ return;
71
+ }
72
+
73
+ // src/index.ts
74
+ function findRoutesDir(fromFile, exists = import_node_fs.existsSync) {
75
+ let dir = import_node_path.dirname(fromFile);
76
+ for (let i = 0;i < 64; i++) {
77
+ if (exists(import_node_path.join(dir, "routes")))
78
+ return import_node_path.join(dir, "routes");
79
+ const parent = import_node_path.dirname(dir);
80
+ if (parent === dir)
81
+ return;
82
+ dir = parent;
83
+ }
84
+ return;
85
+ }
86
+ function innermostNodeAt(tsm, source, node, position) {
87
+ if (position < node.getStart(source) || position > node.getEnd())
88
+ return;
89
+ let found = node;
90
+ node.forEachChild((child) => {
91
+ const inChild = innermostNodeAt(tsm, source, child, position);
92
+ if (inChild !== undefined)
93
+ found = inChild;
94
+ });
95
+ return found;
96
+ }
97
+ function findRoutePathLiteral(tsm, source, position) {
98
+ const node = innermostNodeAt(tsm, source, source, position);
99
+ if (node === undefined || !tsm.isStringLiteralLike(node))
100
+ return;
101
+ if (!node.text.startsWith("/"))
102
+ return;
103
+ const start = node.getStart(source) + 1;
104
+ return { text: node.text, span: { start, length: Math.max(0, node.getWidth(source) - 2) } };
105
+ }
106
+ function routeDefinitionAt(tsm, ls, fileName, position) {
107
+ const source = ls.getProgram()?.getSourceFile(fileName);
108
+ if (source === undefined)
109
+ return;
110
+ const literal = findRoutePathLiteral(tsm, source, position);
111
+ if (literal === undefined)
112
+ return;
113
+ const routesDir = findRoutesDir(fileName);
114
+ if (routesDir === undefined)
115
+ return;
116
+ let file;
117
+ try {
118
+ file = resolveRouteFile(literal.text, import_fs.discoverRoutes(routesDir).routes);
119
+ } catch {
120
+ return;
121
+ }
122
+ if (file === undefined)
123
+ return;
124
+ const target = import_node_path.isAbsolute(file) ? file : import_node_path.join(routesDir, file);
125
+ if (!import_node_fs.existsSync(target))
126
+ return;
127
+ return {
128
+ definitions: [
129
+ {
130
+ fileName: target,
131
+ textSpan: { start: 0, length: 0 },
132
+ kind: tsm.ScriptElementKind.moduleElement,
133
+ name: literal.text,
134
+ containerName: "",
135
+ containerKind: tsm.ScriptElementKind.unknown
136
+ }
137
+ ],
138
+ textSpan: literal.span
139
+ };
140
+ }
141
+ function init(modules) {
142
+ const tsm = modules.typescript;
143
+ return {
144
+ create(info) {
145
+ const ls = info.languageService;
146
+ const proxy = Object.create(null);
147
+ const writable = proxy;
148
+ for (const key of Object.keys(ls)) {
149
+ const member = ls[key];
150
+ writable[key] = typeof member === "function" ? member.bind(ls) : member;
151
+ }
152
+ proxy.getDefinitionAndBoundSpan = (fileName, position) => routeDefinitionAt(tsm, ls, fileName, position) ?? ls.getDefinitionAndBoundSpan(fileName, position);
153
+ return proxy;
154
+ }
155
+ };
156
+ }
157
+ var src_default = init;
158
+
159
+ module.exports = module.exports && module.exports.default ? module.exports.default : module.exports
@@ -0,0 +1,5 @@
1
+ import type * as ts from "typescript";
2
+
3
+ declare function init(modules: { typescript: typeof ts }): ts.server.PluginModule;
4
+
5
+ export = init;
@@ -0,0 +1,17 @@
1
+ import type * as ts from "typescript";
2
+ /** Walk up from a source file to the nearest directory that has a `routes/` folder - the app root. */
3
+ export declare function findRoutesDir(fromFile: string, exists?: (p: string) => boolean): string | undefined;
4
+ /**
5
+ * If `position` sits on a string-literal route path (`"/..."`), return its text and the span INSIDE the
6
+ * quotes (so the editor highlights the path, not the quotes). Exported for unit testing against a
7
+ * `SourceFile` built with `ts.createSourceFile`.
8
+ */
9
+ export declare function findRoutePathLiteral(tsm: typeof ts, source: ts.SourceFile, position: number): {
10
+ text: string;
11
+ span: ts.TextSpan;
12
+ } | undefined;
13
+ declare function init(modules: {
14
+ typescript: typeof ts;
15
+ }): ts.server.PluginModule;
16
+ export default init;
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,KAAK,EAAE,MAAM,YAAY,CAAA;AAGrC,sGAAsG;AACtG,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAoB,GAC1C,MAAM,GAAG,SAAS,CASpB;AAkBD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,OAAO,EAAE,EACd,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,QAAQ,EAAE,MAAM,GACf;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAA;CAAE,GAAG,SAAS,CAMjD;AAuCD,iBAAS,IAAI,CAAC,OAAO,EAAE;IAAE,UAAU,EAAE,OAAO,EAAE,CAAA;CAAE,GAAG,EAAE,CAAC,MAAM,CAAC,YAAY,CAuBxE;AAED,eAAe,IAAI,CAAA"}
@@ -0,0 +1,12 @@
1
+ /** A route as the plugin needs it: its URL pattern and the source file that serves it. */
2
+ export interface RouteLocation {
3
+ readonly pattern: string;
4
+ readonly file: string;
5
+ }
6
+ /**
7
+ * Resolve a route path literal to the first route whose pattern matches it. Query/hash are ignored
8
+ * (a link's `?tab=x` does not change which route file serves it). Returns the route's `file`, or
9
+ * undefined when the value is not a path or matches no route.
10
+ */
11
+ export declare function resolveRouteFile(path: string, routes: readonly RouteLocation[]): string | undefined;
12
+ //# sourceMappingURL=resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAYA,0FAA0F;AAC1F,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAsBD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,SAAS,aAAa,EAAE,GAC/B,MAAM,GAAG,SAAS,CASpB"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The pure half of the plugin: resolve a concrete route path (a string literal the user typed, e.g.
3
+ * `"/users/42"`) to the `routes/` file that serves it. It matches the path against each route's compiled
4
+ * pattern using nifra's OWN router matcher (`@nifrajs/core/pattern`), so a path resolves here exactly as
5
+ * it would at runtime - the plugin never re-derives the file-based routing rules.
6
+ */
7
+ import { compileRoutePattern, matchRoutePattern, sortRoutesBySpecificity, } from "@nifrajs/core/pattern";
8
+ // The language service asks for definitions repeatedly while the cursor moves. Cache the immutable
9
+ // compilation/specificity sort by manifest identity so each query only performs route matching; a new
10
+ // manifest array naturally gets a fresh entry when routes are added or removed.
11
+ const compiledRouteCache = new WeakMap();
12
+ function compiledRoutes(routes) {
13
+ const cached = compiledRouteCache.get(routes);
14
+ if (cached !== undefined)
15
+ return cached;
16
+ const compiled = sortRoutesBySpecificity(routes.map((route) => ({ route, pattern: compileRoutePattern(route.pattern) })));
17
+ compiledRouteCache.set(routes, compiled);
18
+ return compiled;
19
+ }
20
+ /**
21
+ * Resolve a route path literal to the first route whose pattern matches it. Query/hash are ignored
22
+ * (a link's `?tab=x` does not change which route file serves it). Returns the route's `file`, or
23
+ * undefined when the value is not a path or matches no route.
24
+ */
25
+ export function resolveRouteFile(path, routes) {
26
+ if (!path.startsWith("/"))
27
+ return undefined;
28
+ const pathname = path.split(/[?#]/, 1)[0] ?? path;
29
+ // Runtime/client routing sorts by specificity before matching, so a static route wins over a
30
+ // dynamic one regardless of manifest discovery order.
31
+ for (const { route, pattern } of compiledRoutes(routes)) {
32
+ if (matchRoutePattern(pattern, pathname).matched)
33
+ return route.file;
34
+ }
35
+ return undefined;
36
+ }
37
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../src/resolve.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,mBAAmB,EACnB,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,uBAAuB,CAAA;AAa9B,mGAAmG;AACnG,sGAAsG;AACtG,gFAAgF;AAChF,MAAM,kBAAkB,GAAG,IAAI,OAAO,EAA8D,CAAA;AAEpG,SAAS,cAAc,CAAC,MAAgC;IACtD,MAAM,MAAM,GAAG,kBAAkB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAC7C,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAA;IACvC,MAAM,QAAQ,GAAG,uBAAuB,CACtC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,mBAAmB,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAChF,CAAA;IACD,kBAAkB,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IACxC,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC9B,IAAY,EACZ,MAAgC;IAEhC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAA;IAC3C,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAA;IACjD,6FAA6F;IAC7F,sDAAsD;IACtD,KAAK,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC;QACxD,IAAI,iBAAiB,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,OAAO;YAAE,OAAO,KAAK,CAAC,IAAI,CAAA;IACrE,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@nifrajs/ts-plugin",
3
+ "version": "2.4.0",
4
+ "description": "TypeScript language-service plugin for nifra - go-to-definition from a route-path string literal to the routes/ file that serves it.",
5
+ "keywords": [
6
+ "bun",
7
+ "typescript",
8
+ "nifra",
9
+ "web-framework",
10
+ "full-stack",
11
+ "tsserver",
12
+ "language-service",
13
+ "editor"
14
+ ],
15
+ "type": "module",
16
+ "main": "./dist/index.cjs",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "bun": "./src/index.ts",
21
+ "import": {
22
+ "types": "./dist/index.d.cts",
23
+ "default": "./dist/index.cjs"
24
+ },
25
+ "require": {
26
+ "types": "./dist/index.d.cts",
27
+ "default": "./dist/index.cjs"
28
+ },
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.cjs"
31
+ },
32
+ "./resolve": {
33
+ "bun": "./src/resolve.ts",
34
+ "types": "./dist/resolve.d.ts",
35
+ "default": "./dist/resolve.js"
36
+ }
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "src"
41
+ ],
42
+ "sideEffects": false,
43
+ "scripts": {
44
+ "build": "bun run build.ts"
45
+ },
46
+ "dependencies": {
47
+ "@nifrajs/core": "2.4.0",
48
+ "@nifrajs/web": "2.4.0"
49
+ },
50
+ "peerDependencies": {
51
+ "typescript": ">=5.5.0"
52
+ },
53
+ "license": "MIT",
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "git+https://github.com/nifrajs/nifra.git",
60
+ "directory": "packages/ts-plugin"
61
+ },
62
+ "bugs": {
63
+ "url": "https://github.com/nifrajs/nifra/issues"
64
+ },
65
+ "homepage": "https://github.com/nifrajs/nifra#readme"
66
+ }
package/src/index.ts ADDED
@@ -0,0 +1,131 @@
1
+ /**
2
+ * `@nifrajs/ts-plugin` - a TypeScript language-service plugin. It adds ONE thing to the editor:
3
+ * go-to-definition on a route-path string literal jumps to the `routes/` file that serves it, so
4
+ * `navigate({ to: "/orders" })`, `<Link to="/orders">`, or `href="/orders"` are click-through.
5
+ *
6
+ * Wire it up in a project's tsconfig:
7
+ * ```json
8
+ * { "compilerOptions": { "plugins": [{ "name": "@nifrajs/ts-plugin" }] } }
9
+ * ```
10
+ *
11
+ * The routing rules are nifra's own: it discovers routes with `@nifrajs/web`'s `discoverRoutes` and
12
+ * matches with `@nifrajs/core`'s pattern matcher (see ./resolve.ts), so a path resolves to exactly the
13
+ * file it would serve at runtime. The plugin only wires that into `getDefinitionAndBoundSpan`.
14
+ */
15
+ import { existsSync } from "node:fs"
16
+ import { dirname, isAbsolute, join } from "node:path"
17
+ import { discoverRoutes } from "@nifrajs/web/fs"
18
+ import type * as ts from "typescript"
19
+ import { resolveRouteFile } from "./resolve.ts"
20
+
21
+ /** Walk up from a source file to the nearest directory that has a `routes/` folder - the app root. */
22
+ export function findRoutesDir(
23
+ fromFile: string,
24
+ exists: (p: string) => boolean = existsSync,
25
+ ): string | undefined {
26
+ let dir = dirname(fromFile)
27
+ for (let i = 0; i < 64; i++) {
28
+ if (exists(join(dir, "routes"))) return join(dir, "routes")
29
+ const parent = dirname(dir)
30
+ if (parent === dir) return undefined
31
+ dir = parent
32
+ }
33
+ return undefined
34
+ }
35
+
36
+ /** The innermost AST node whose span contains `position`. */
37
+ function innermostNodeAt(
38
+ tsm: typeof ts,
39
+ source: ts.SourceFile,
40
+ node: ts.Node,
41
+ position: number,
42
+ ): ts.Node | undefined {
43
+ if (position < node.getStart(source) || position > node.getEnd()) return undefined
44
+ let found: ts.Node = node
45
+ node.forEachChild((child) => {
46
+ const inChild = innermostNodeAt(tsm, source, child, position)
47
+ if (inChild !== undefined) found = inChild
48
+ })
49
+ return found
50
+ }
51
+
52
+ /**
53
+ * If `position` sits on a string-literal route path (`"/..."`), return its text and the span INSIDE the
54
+ * quotes (so the editor highlights the path, not the quotes). Exported for unit testing against a
55
+ * `SourceFile` built with `ts.createSourceFile`.
56
+ */
57
+ export function findRoutePathLiteral(
58
+ tsm: typeof ts,
59
+ source: ts.SourceFile,
60
+ position: number,
61
+ ): { text: string; span: ts.TextSpan } | undefined {
62
+ const node = innermostNodeAt(tsm, source, source, position)
63
+ if (node === undefined || !tsm.isStringLiteralLike(node)) return undefined
64
+ if (!node.text.startsWith("/")) return undefined
65
+ const start = node.getStart(source) + 1 // step past the opening quote
66
+ return { text: node.text, span: { start, length: Math.max(0, node.getWidth(source) - 2) } }
67
+ }
68
+
69
+ /** Build the route go-to-definition for the literal at `position`, or undefined to fall through. */
70
+ function routeDefinitionAt(
71
+ tsm: typeof ts,
72
+ ls: ts.LanguageService,
73
+ fileName: string,
74
+ position: number,
75
+ ): ts.DefinitionInfoAndBoundSpan | undefined {
76
+ const source = ls.getProgram()?.getSourceFile(fileName)
77
+ if (source === undefined) return undefined
78
+ const literal = findRoutePathLiteral(tsm, source, position)
79
+ if (literal === undefined) return undefined
80
+ const routesDir = findRoutesDir(fileName)
81
+ if (routesDir === undefined) return undefined
82
+ let file: string | undefined
83
+ try {
84
+ file = resolveRouteFile(literal.text, discoverRoutes(routesDir).routes)
85
+ } catch {
86
+ return undefined // a routes/ dir that can't be scanned is not this plugin's error to raise
87
+ }
88
+ if (file === undefined) return undefined
89
+ const target = isAbsolute(file) ? file : join(routesDir, file)
90
+ if (!existsSync(target)) return undefined
91
+ return {
92
+ definitions: [
93
+ {
94
+ fileName: target,
95
+ textSpan: { start: 0, length: 0 },
96
+ kind: tsm.ScriptElementKind.moduleElement,
97
+ name: literal.text,
98
+ containerName: "",
99
+ containerKind: tsm.ScriptElementKind.unknown,
100
+ },
101
+ ],
102
+ textSpan: literal.span,
103
+ }
104
+ }
105
+
106
+ function init(modules: { typescript: typeof ts }): ts.server.PluginModule {
107
+ const tsm = modules.typescript
108
+ return {
109
+ create(info: ts.server.PluginCreateInfo): ts.LanguageService {
110
+ const ls = info.languageService
111
+ // Proxy the language service, delegating every method to the real one except the one we augment.
112
+ // Plain ASSIGNMENT (not Object.defineProperty) so each property stays writable - the override
113
+ // below reassigns one, which a non-writable property would reject at runtime.
114
+ const proxy = Object.create(null) as ts.LanguageService
115
+ const writable = proxy as unknown as Record<string, unknown>
116
+ for (const key of Object.keys(ls) as Array<keyof ts.LanguageService>) {
117
+ const member = ls[key]
118
+ writable[key] =
119
+ typeof member === "function"
120
+ ? (member as (...args: unknown[]) => unknown).bind(ls)
121
+ : member
122
+ }
123
+ proxy.getDefinitionAndBoundSpan = (fileName, position) =>
124
+ routeDefinitionAt(tsm, ls, fileName, position) ??
125
+ ls.getDefinitionAndBoundSpan(fileName, position)
126
+ return proxy
127
+ },
128
+ }
129
+ }
130
+
131
+ export default init
package/src/resolve.ts ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The pure half of the plugin: resolve a concrete route path (a string literal the user typed, e.g.
3
+ * `"/users/42"`) to the `routes/` file that serves it. It matches the path against each route's compiled
4
+ * pattern using nifra's OWN router matcher (`@nifrajs/core/pattern`), so a path resolves here exactly as
5
+ * it would at runtime - the plugin never re-derives the file-based routing rules.
6
+ */
7
+ import {
8
+ compileRoutePattern,
9
+ matchRoutePattern,
10
+ sortRoutesBySpecificity,
11
+ } from "@nifrajs/core/pattern"
12
+
13
+ /** A route as the plugin needs it: its URL pattern and the source file that serves it. */
14
+ export interface RouteLocation {
15
+ readonly pattern: string
16
+ readonly file: string
17
+ }
18
+
19
+ type CompiledRouteLocation = {
20
+ readonly route: RouteLocation
21
+ readonly pattern: ReturnType<typeof compileRoutePattern>
22
+ }
23
+
24
+ // The language service asks for definitions repeatedly while the cursor moves. Cache the immutable
25
+ // compilation/specificity sort by manifest identity so each query only performs route matching; a new
26
+ // manifest array naturally gets a fresh entry when routes are added or removed.
27
+ const compiledRouteCache = new WeakMap<readonly RouteLocation[], readonly CompiledRouteLocation[]>()
28
+
29
+ function compiledRoutes(routes: readonly RouteLocation[]): readonly CompiledRouteLocation[] {
30
+ const cached = compiledRouteCache.get(routes)
31
+ if (cached !== undefined) return cached
32
+ const compiled = sortRoutesBySpecificity(
33
+ routes.map((route) => ({ route, pattern: compileRoutePattern(route.pattern) })),
34
+ )
35
+ compiledRouteCache.set(routes, compiled)
36
+ return compiled
37
+ }
38
+
39
+ /**
40
+ * Resolve a route path literal to the first route whose pattern matches it. Query/hash are ignored
41
+ * (a link's `?tab=x` does not change which route file serves it). Returns the route's `file`, or
42
+ * undefined when the value is not a path or matches no route.
43
+ */
44
+ export function resolveRouteFile(
45
+ path: string,
46
+ routes: readonly RouteLocation[],
47
+ ): string | undefined {
48
+ if (!path.startsWith("/")) return undefined
49
+ const pathname = path.split(/[?#]/, 1)[0] ?? path
50
+ // Runtime/client routing sorts by specificity before matching, so a static route wins over a
51
+ // dynamic one regardless of manifest discovery order.
52
+ for (const { route, pattern } of compiledRoutes(routes)) {
53
+ if (matchRoutePattern(pattern, pathname).matched) return route.file
54
+ }
55
+ return undefined
56
+ }