@nifrajs/ts-plugin 3.5.0 → 4.0.2

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 CHANGED
@@ -4,6 +4,8 @@ A TypeScript language-service plugin for [nifra](https://github.com/nifrajs/nifr
4
4
 
5
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
6
 
7
+ It also shows the frontend/backend zone rules as you type: a value import that the build would refuse - backend code in a page, frontend code in `backend/`, anything but shared code in `shared/`, a Node built-in or a server package (`pg`, a mailer, a server SDK) in browser code - is an error on the import itself. `import type` is always allowed. The rules come from `@nifrajs/web/zones`, the same classifier the builds and `nifra check` use; the build stays authoritative and reports the full import chain.
8
+
7
9
  ## Install
8
10
 
9
11
  ```sh
package/dist/index.cjs CHANGED
@@ -44,8 +44,8 @@ __export(exports_src, {
44
44
  findRoutesDir: () => findRoutesDir
45
45
  });
46
46
  module.exports = __toCommonJS(exports_src);
47
- var import_node_fs = require("node:fs");
48
- var import_node_path = require("node:path");
47
+ var import_node_fs2 = require("node:fs");
48
+ var import_node_path2 = require("node:path");
49
49
  var import_fs = require("@nifrajs/web/fs");
50
50
 
51
51
  // src/resolve.ts
@@ -70,13 +70,128 @@ function resolveRouteFile(path, routes) {
70
70
  return;
71
71
  }
72
72
 
73
+ // src/zones.ts
74
+ var import_node_fs = require("node:fs");
75
+ var import_node_path = require("node:path");
76
+ var import_zones = require("@nifrajs/web/zones");
77
+ var ZONE_DIAGNOSTIC_CODE = 990028;
78
+ var BROWSER_CODE = new Set(["route-frontend", "frontend", "shared"]);
79
+ var allTypes = (elements) => elements.length > 0 && elements.every((element) => element.isTypeOnly);
80
+ function valueImports(tsm, source) {
81
+ const edges = [];
82
+ const add = (node) => {
83
+ if (node !== undefined && tsm.isStringLiteralLike(node)) {
84
+ edges.push({ specifier: node.text, literal: node });
85
+ }
86
+ };
87
+ const visit = (node) => {
88
+ if (tsm.isImportDeclaration(node)) {
89
+ const clause = node.importClause;
90
+ const bindings = clause?.namedBindings;
91
+ const typeOnly = clause !== undefined && (clause.isTypeOnly || clause.name === undefined && bindings !== undefined && tsm.isNamedImports(bindings) && allTypes(bindings.elements));
92
+ if (!typeOnly)
93
+ add(node.moduleSpecifier);
94
+ return;
95
+ }
96
+ if (tsm.isExportDeclaration(node)) {
97
+ const clause = node.exportClause;
98
+ const typeOnly = node.isTypeOnly || clause !== undefined && tsm.isNamedExports(clause) && allTypes(clause.elements);
99
+ if (!typeOnly)
100
+ add(node.moduleSpecifier);
101
+ return;
102
+ }
103
+ if (tsm.isImportEqualsDeclaration(node) && !node.isTypeOnly && tsm.isExternalModuleReference(node.moduleReference)) {
104
+ add(node.moduleReference.expression);
105
+ return;
106
+ }
107
+ if (tsm.isCallExpression(node) && node.expression.kind === tsm.SyntaxKind.ImportKeyword) {
108
+ add(node.arguments[0]);
109
+ }
110
+ tsm.forEachChild(node, visit);
111
+ };
112
+ visit(source);
113
+ return edges;
114
+ }
115
+ function resolveImport(tsm, source, edge, options) {
116
+ const specifier = edge.specifier.replace(/[?#].*$/, "");
117
+ const mode = tsm.getModeForUsageLocation(source, edge.literal, options);
118
+ const resolved = tsm.resolveModuleName(specifier, source.fileName, options, tsm.sys, undefined, undefined, mode).resolvedModule?.resolvedFileName;
119
+ if (resolved !== undefined)
120
+ return resolved;
121
+ if (!specifier.startsWith("./") && !specifier.startsWith("../"))
122
+ return;
123
+ const file = import_node_path.resolve(import_node_path.dirname(source.fileName), specifier);
124
+ return import_node_fs.existsSync(file) ? file : undefined;
125
+ }
126
+ var real = (file) => {
127
+ try {
128
+ return import_node_fs.realpathSync.native(file);
129
+ } catch {
130
+ return file;
131
+ }
132
+ };
133
+ function libraryAsImported(target, specifier) {
134
+ if (target.zone !== "library")
135
+ return target;
136
+ const named = import_zones.specifierPackage(specifier);
137
+ if (named === undefined || named === target.packageName)
138
+ return target;
139
+ return { zone: "library", packageName: named };
140
+ }
141
+ function edgeProblem(classifier, display, fileName, from, specifier, target) {
142
+ const browser = BROWSER_CODE.has(from);
143
+ if (browser && /^(?:node|bun):/.test(specifier)) {
144
+ return `"${specifier}" may not reach the browser: Node and Bun built-ins run on the server only; use them in a route's backend half or under backend/`;
145
+ }
146
+ if (browser && specifier === import_zones.BACKEND_ONLY_MARKER) {
147
+ return `"${import_zones.BACKEND_ONLY_MARKER}" marks a module backend-only, but ${display(fileName)} reaches the browser. Move it under backend/ or into a route's backend half`;
148
+ }
149
+ if (target === undefined)
150
+ return;
151
+ const to = classifier.classify(target);
152
+ if (to.zone === "error")
153
+ return `${display(fileName)} imports ${display(target)}: ${to.reason}`;
154
+ if (to.zone === "library") {
155
+ const reason = browser ? import_zones.browserDenial(libraryAsImported(to, specifier), specifier) : undefined;
156
+ return reason === undefined ? undefined : `"${specifier}" may not reach the browser: ${reason}`;
157
+ }
158
+ return import_zones.importAllowed(from, to.zone) ? undefined : import_zones.importRuleMessage(display(fileName), from, display(target), to.zone);
159
+ }
160
+ function zoneDiagnostics(tsm, source, appRoot, options) {
161
+ const classifier = import_zones.createZoneClassifier({ appRoot });
162
+ const from = classifier.classify(source.fileName);
163
+ if (from.zone === "error" || from.zone === "library" || from.zone === "generated")
164
+ return [];
165
+ const display = (file) => {
166
+ const rel = import_node_path.relative(classifier.appRoot, real(file)).replaceAll("\\", "/");
167
+ return rel.startsWith("../") || import_node_path.isAbsolute(rel) ? file : rel;
168
+ };
169
+ const diagnostics = [];
170
+ for (const edge of valueImports(tsm, source)) {
171
+ const target = resolveImport(tsm, source, edge, options);
172
+ const message = edgeProblem(classifier, display, source.fileName, from.zone, edge.specifier, target);
173
+ if (message === undefined)
174
+ continue;
175
+ diagnostics.push({
176
+ file: source,
177
+ start: edge.literal.getStart(source),
178
+ length: edge.literal.getWidth(source),
179
+ messageText: message,
180
+ category: tsm.DiagnosticCategory.Error,
181
+ code: ZONE_DIAGNOSTIC_CODE,
182
+ source: "nifra"
183
+ });
184
+ }
185
+ return diagnostics;
186
+ }
187
+
73
188
  // src/index.ts
74
- function findRoutesDir(fromFile, exists = import_node_fs.existsSync) {
75
- let dir = import_node_path.dirname(fromFile);
189
+ function findRoutesDir(fromFile, exists = import_node_fs2.existsSync) {
190
+ let dir = import_node_path2.dirname(fromFile);
76
191
  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);
192
+ if (exists(import_node_path2.join(dir, "routes")))
193
+ return import_node_path2.join(dir, "routes");
194
+ const parent = import_node_path2.dirname(dir);
80
195
  if (parent === dir)
81
196
  return;
82
197
  dir = parent;
@@ -89,13 +204,13 @@ function normalizeProgramPath(fileName) {
89
204
  return process.platform === "win32" ? withoutDevicePrefix.toLowerCase() : withoutDevicePrefix;
90
205
  }
91
206
  function canonicalProgramPaths(tsm, fileName) {
92
- const absolute = import_node_path.resolve(fileName);
207
+ const absolute = import_node_path2.resolve(fileName);
93
208
  const paths = new Set([normalizeProgramPath(absolute)]);
94
209
  const tsmRealpath = tsm.sys.realpath;
95
210
  const resolvers = [
96
211
  ...typeof tsmRealpath === "function" ? [tsmRealpath] : [],
97
- import_node_fs.realpathSync.native,
98
- import_node_fs.realpathSync
212
+ import_node_fs2.realpathSync.native,
213
+ import_node_fs2.realpathSync
99
214
  ];
100
215
  for (const resolver of resolvers) {
101
216
  try {
@@ -110,20 +225,20 @@ function typeScriptProgramPath(tsm, program, fileName) {
110
225
  return;
111
226
  const getCanonicalFileName = tsm.sys.useCaseSensitiveFileNames ? (value) => value : (value) => value.toLowerCase();
112
227
  try {
113
- return toPath(import_node_path.resolve(fileName), program.getCurrentDirectory(), getCanonicalFileName);
228
+ return toPath(import_node_path2.resolve(fileName), program.getCurrentDirectory(), getCanonicalFileName);
114
229
  } catch {
115
230
  return;
116
231
  }
117
232
  }
118
233
  function filesystemIdentity(fileName) {
119
234
  try {
120
- const info = import_node_fs.statSync(import_node_path.resolve(fileName), { bigint: true });
235
+ const info = import_node_fs2.statSync(import_node_path2.resolve(fileName), { bigint: true });
121
236
  if (info.ino === 0n)
122
237
  return;
123
238
  return `bigint:${info.dev}:${info.ino}`;
124
239
  } catch {
125
240
  try {
126
- const info = import_node_fs.statSync(import_node_path.resolve(fileName));
241
+ const info = import_node_fs2.statSync(import_node_path2.resolve(fileName));
127
242
  if (info.ino === 0)
128
243
  return;
129
244
  return `number:${info.dev}:${info.ino}`;
@@ -167,7 +282,7 @@ function hostSourceFile(tsm, host, program, fileName) {
167
282
  function diskSourceFile(tsm, program, fileName) {
168
283
  let text;
169
284
  try {
170
- text = import_node_fs.readFileSync(fileName, "utf8");
285
+ text = import_node_fs2.readFileSync(fileName, "utf8");
171
286
  } catch {
172
287
  return;
173
288
  }
@@ -213,8 +328,8 @@ function routeDefinitionAt(tsm, ls, host, fileName, position) {
213
328
  }
214
329
  if (file === undefined)
215
330
  return;
216
- const target = import_node_path.isAbsolute(file) ? file : import_node_path.join(routesDir, file);
217
- if (!import_node_fs.existsSync(target))
331
+ const target = import_node_path2.isAbsolute(file) ? file : import_node_path2.join(routesDir, file);
332
+ if (!import_node_fs2.existsSync(target))
218
333
  return;
219
334
  return {
220
335
  definitions: [
@@ -251,6 +366,19 @@ function init(modules) {
251
366
  const source = programSourceFile(tsm, ls.getProgram(), fileName);
252
367
  return source === undefined ? undefined : ls.getDefinitionAndBoundSpan(source.fileName, position);
253
368
  })();
369
+ proxy.getSemanticDiagnostics = (fileName) => {
370
+ const source = programSourceFile(tsm, ls.getProgram(), fileName);
371
+ const own = ls.getSemanticDiagnostics(source?.fileName ?? fileName);
372
+ const routesDir = source === undefined ? undefined : findRoutesDir(source.fileName);
373
+ if (source === undefined || routesDir === undefined)
374
+ return own;
375
+ try {
376
+ const options = ls.getProgram()?.getCompilerOptions() ?? {};
377
+ return [...own, ...zoneDiagnostics(tsm, source, import_node_path2.dirname(routesDir), options)];
378
+ } catch {
379
+ return own;
380
+ }
381
+ };
254
382
  return proxy;
255
383
  }
256
384
  };
@@ -1 +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;AAgMD;;;;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;AA4CD,iBAAS,IAAI,CAAC,OAAO,EAAE;IAAE,UAAU,EAAE,OAAO,EAAE,CAAA;CAAE,GAAG,EAAE,CAAC,MAAM,CAAC,YAAY,CA6CxE;AAED,eAAe,IAAI,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,KAAK,EAAE,MAAM,YAAY,CAAA;AAIrC,sGAAsG;AACtG,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,CAAC,CAAC,EAAE,MAAM,KAAK,OAAoB,GAC1C,MAAM,GAAG,SAAS,CASpB;AAgMD;;;;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;AA4CD,iBAAS,IAAI,CAAC,OAAO,EAAE;IAAE,UAAU,EAAE,OAAO,EAAE,CAAA;CAAE,GAAG,EAAE,CAAC,MAAM,CAAC,YAAY,CAyDxE;AAED,eAAe,IAAI,CAAA"}
@@ -0,0 +1,9 @@
1
+ import type * as ts from "typescript";
2
+ /** The code every zone diagnostic carries; TypeScript requires a number, and none of its own is this. */
3
+ export declare const ZONE_DIAGNOSTIC_CODE = 990028;
4
+ /**
5
+ * The zone diagnostics for one file of the app rooted at `appRoot`. A fresh classifier per call keeps a
6
+ * moved file or an edited package declaration from going stale; it costs a few `stat`s per import.
7
+ */
8
+ export declare function zoneDiagnostics(tsm: typeof ts, source: ts.SourceFile, appRoot: string, options: ts.CompilerOptions): ts.Diagnostic[];
9
+ //# sourceMappingURL=zones.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zones.d.ts","sourceRoot":"","sources":["../src/zones.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,KAAK,EAAE,MAAM,YAAY,CAAA;AAErC,yGAAyG;AACzG,eAAO,MAAM,oBAAoB,SAAS,CAAA;AAmI1C;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,OAAO,EAAE,EACd,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,EAAE,CAAC,eAAe,GAC1B,EAAE,CAAC,UAAU,EAAE,CA+BjB"}
package/dist/zones.js ADDED
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Zone diagnostics: the frontend/backend import rules of `@nifrajs/web/zones`, in the editor.
3
+ *
4
+ * Assist only. The browser builds, the dev servers and `nifra check` enforce the same rules with the
5
+ * full import chain; this reports the direct edge as it is typed, so a backend import in a page is
6
+ * red before anything is built. Files in no zone (tests, scripts, config) are never reported: no
7
+ * build loads them.
8
+ */
9
+ import { existsSync, realpathSync } from "node:fs";
10
+ import { dirname, isAbsolute, relative, resolve } from "node:path";
11
+ import { BACKEND_ONLY_MARKER, browserDenial, createZoneClassifier, importAllowed, importRuleMessage, specifierPackage, } from "@nifrajs/web/zones";
12
+ /** The code every zone diagnostic carries; TypeScript requires a number, and none of its own is this. */
13
+ export const ZONE_DIAGNOSTIC_CODE = 990028;
14
+ /** Zones whose own code runs in a browser. */
15
+ const BROWSER_CODE = new Set(["route-frontend", "frontend", "shared"]);
16
+ const allTypes = (elements) => elements.length > 0 && elements.every((element) => element.isTypeOnly);
17
+ /** Every value import in a file: static, re-export and literal dynamic `import()`. Type-only ones are
18
+ * erased before bundling, so they may cross zones. */
19
+ function valueImports(tsm, source) {
20
+ const edges = [];
21
+ const add = (node) => {
22
+ if (node !== undefined && tsm.isStringLiteralLike(node)) {
23
+ edges.push({ specifier: node.text, literal: node });
24
+ }
25
+ };
26
+ const visit = (node) => {
27
+ if (tsm.isImportDeclaration(node)) {
28
+ const clause = node.importClause;
29
+ const bindings = clause?.namedBindings;
30
+ const typeOnly = clause !== undefined &&
31
+ (clause.isTypeOnly ||
32
+ (clause.name === undefined &&
33
+ bindings !== undefined &&
34
+ tsm.isNamedImports(bindings) &&
35
+ allTypes(bindings.elements)));
36
+ if (!typeOnly)
37
+ add(node.moduleSpecifier);
38
+ return;
39
+ }
40
+ if (tsm.isExportDeclaration(node)) {
41
+ const clause = node.exportClause;
42
+ const typeOnly = node.isTypeOnly ||
43
+ (clause !== undefined && tsm.isNamedExports(clause) && allTypes(clause.elements));
44
+ if (!typeOnly)
45
+ add(node.moduleSpecifier);
46
+ return;
47
+ }
48
+ if (tsm.isImportEqualsDeclaration(node) &&
49
+ !node.isTypeOnly &&
50
+ tsm.isExternalModuleReference(node.moduleReference)) {
51
+ add(node.moduleReference.expression);
52
+ return;
53
+ }
54
+ if (tsm.isCallExpression(node) && node.expression.kind === tsm.SyntaxKind.ImportKeyword) {
55
+ add(node.arguments[0]);
56
+ }
57
+ tsm.forEachChild(node, visit);
58
+ };
59
+ visit(source);
60
+ return edges;
61
+ }
62
+ /** Where an import lands: TypeScript's resolution (tsconfig `paths` included), else a relative path
63
+ * that exists on disk (an `.svelte` or `.vue` file TypeScript only knows through a shim). */
64
+ function resolveImport(tsm, source, edge, options) {
65
+ const specifier = edge.specifier.replace(/[?#].*$/, "");
66
+ const mode = tsm.getModeForUsageLocation(source, edge.literal, options);
67
+ const resolved = tsm.resolveModuleName(specifier, source.fileName, options, tsm.sys, undefined, undefined, mode).resolvedModule?.resolvedFileName;
68
+ if (resolved !== undefined)
69
+ return resolved;
70
+ if (!specifier.startsWith("./") && !specifier.startsWith("../"))
71
+ return undefined;
72
+ const file = resolve(dirname(source.fileName), specifier);
73
+ return existsSync(file) ? file : undefined;
74
+ }
75
+ const real = (file) => {
76
+ try {
77
+ return realpathSync.native(file);
78
+ }
79
+ catch {
80
+ return file;
81
+ }
82
+ };
83
+ /** A third-party target as the specifier names it: TypeScript resolves `pg` to `@types/pg`, and the
84
+ * server-package list knows `pg`. */
85
+ function libraryAsImported(target, specifier) {
86
+ if (target.zone !== "library")
87
+ return target;
88
+ const named = specifierPackage(specifier);
89
+ if (named === undefined || named === target.packageName)
90
+ return target;
91
+ return { zone: "library", packageName: named };
92
+ }
93
+ /** Why one import breaks the zone rules, or `undefined` when it does not. */
94
+ function edgeProblem(classifier, display, fileName, from, specifier, target) {
95
+ const browser = BROWSER_CODE.has(from);
96
+ if (browser && /^(?:node|bun):/.test(specifier)) {
97
+ return `"${specifier}" may not reach the browser: Node and Bun built-ins run on the server only; use them in a route's backend half or under backend/`;
98
+ }
99
+ if (browser && specifier === BACKEND_ONLY_MARKER) {
100
+ return `"${BACKEND_ONLY_MARKER}" marks a module backend-only, but ${display(fileName)} reaches the browser. Move it under backend/ or into a route's backend half`;
101
+ }
102
+ if (target === undefined)
103
+ return undefined;
104
+ const to = classifier.classify(target);
105
+ if (to.zone === "error")
106
+ return `${display(fileName)} imports ${display(target)}: ${to.reason}`;
107
+ if (to.zone === "library") {
108
+ const reason = browser ? browserDenial(libraryAsImported(to, specifier), specifier) : undefined;
109
+ return reason === undefined ? undefined : `"${specifier}" may not reach the browser: ${reason}`;
110
+ }
111
+ return importAllowed(from, to.zone)
112
+ ? undefined
113
+ : importRuleMessage(display(fileName), from, display(target), to.zone);
114
+ }
115
+ /**
116
+ * The zone diagnostics for one file of the app rooted at `appRoot`. A fresh classifier per call keeps a
117
+ * moved file or an edited package declaration from going stale; it costs a few `stat`s per import.
118
+ */
119
+ export function zoneDiagnostics(tsm, source, appRoot, options) {
120
+ const classifier = createZoneClassifier({ appRoot });
121
+ const from = classifier.classify(source.fileName);
122
+ if (from.zone === "error" || from.zone === "library" || from.zone === "generated")
123
+ return [];
124
+ const display = (file) => {
125
+ const rel = relative(classifier.appRoot, real(file)).replaceAll("\\", "/");
126
+ return rel.startsWith("../") || isAbsolute(rel) ? file : rel;
127
+ };
128
+ const diagnostics = [];
129
+ for (const edge of valueImports(tsm, source)) {
130
+ const target = resolveImport(tsm, source, edge, options);
131
+ const message = edgeProblem(classifier, display, source.fileName, from.zone, edge.specifier, target);
132
+ if (message === undefined)
133
+ continue;
134
+ diagnostics.push({
135
+ file: source,
136
+ start: edge.literal.getStart(source),
137
+ length: edge.literal.getWidth(source),
138
+ messageText: message,
139
+ category: tsm.DiagnosticCategory.Error,
140
+ code: ZONE_DIAGNOSTIC_CODE,
141
+ source: "nifra",
142
+ });
143
+ }
144
+ return diagnostics;
145
+ }
146
+ //# sourceMappingURL=zones.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zones.js","sourceRoot":"","sources":["../src/zones.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nifrajs/ts-plugin",
3
- "version": "3.5.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.",
3
+ "version": "4.0.2",
4
+ "description": "TypeScript language-service plugin for nifra - go-to-definition from a route-path string literal to the routes/ file that serves it, and the frontend/backend zone rules as editor errors.",
5
5
  "keywords": [
6
6
  "bun",
7
7
  "typescript",
@@ -13,6 +13,9 @@
13
13
  "editor"
14
14
  ],
15
15
  "type": "module",
16
+ "nifra": {
17
+ "environment": "library"
18
+ },
16
19
  "main": "./dist/index.cjs",
17
20
  "types": "./dist/index.d.ts",
18
21
  "exports": {
@@ -44,8 +47,8 @@
44
47
  "build": "bun run build.ts"
45
48
  },
46
49
  "dependencies": {
47
- "@nifrajs/core": "3.5.0",
48
- "@nifrajs/web": "3.5.0"
50
+ "@nifrajs/core": "4.0.2",
51
+ "@nifrajs/web": "4.0.2"
49
52
  },
50
53
  "peerDependencies": {
51
54
  "typescript": ">=5.5.0"
package/src/index.ts CHANGED
@@ -1,7 +1,8 @@
1
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.
2
+ * `@nifrajs/ts-plugin` - a TypeScript language-service plugin. It adds two things 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
+ * - the frontend/backend zone rules as errors on the import that breaks them (see ./zones.ts).
5
6
  *
6
7
  * Wire it up in a project's tsconfig:
7
8
  * ```json
@@ -10,13 +11,14 @@
10
11
  *
11
12
  * The routing rules are nifra's own: it discovers routes with `@nifrajs/web`'s `discoverRoutes` and
12
13
  * 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
+ * file it would serve at runtime; the zone rules are `@nifrajs/web/zones`, the builds' own classifier.
14
15
  */
15
16
  import { existsSync, readFileSync, realpathSync, statSync } from "node:fs"
16
17
  import { dirname, isAbsolute, join, resolve } from "node:path"
17
18
  import { discoverRoutes } from "@nifrajs/web/fs"
18
19
  import type * as ts from "typescript"
19
20
  import { resolveRouteFile } from "./resolve.ts"
21
+ import { zoneDiagnostics } from "./zones.ts"
20
22
 
21
23
  /** Walk up from a source file to the nearest directory that has a `routes/` folder - the app root. */
22
24
  export function findRoutesDir(
@@ -324,6 +326,18 @@ function init(modules: { typescript: typeof ts }): ts.server.PluginModule {
324
326
  ? undefined
325
327
  : ls.getDefinitionAndBoundSpan(source.fileName, position)
326
328
  })()
329
+ proxy.getSemanticDiagnostics = (fileName) => {
330
+ const source = programSourceFile(tsm, ls.getProgram(), fileName)
331
+ const own = ls.getSemanticDiagnostics(source?.fileName ?? fileName)
332
+ const routesDir = source === undefined ? undefined : findRoutesDir(source.fileName)
333
+ if (source === undefined || routesDir === undefined) return own
334
+ try {
335
+ const options = ls.getProgram()?.getCompilerOptions() ?? {}
336
+ return [...own, ...zoneDiagnostics(tsm, source, dirname(routesDir), options)]
337
+ } catch {
338
+ return own // the build enforces the rules; an editor assist must never break diagnostics
339
+ }
340
+ }
327
341
  return proxy
328
342
  },
329
343
  }
package/src/zones.ts ADDED
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Zone diagnostics: the frontend/backend import rules of `@nifrajs/web/zones`, in the editor.
3
+ *
4
+ * Assist only. The browser builds, the dev servers and `nifra check` enforce the same rules with the
5
+ * full import chain; this reports the direct edge as it is typed, so a backend import in a page is
6
+ * red before anything is built. Files in no zone (tests, scripts, config) are never reported: no
7
+ * build loads them.
8
+ */
9
+ import { existsSync, realpathSync } from "node:fs"
10
+ import { dirname, isAbsolute, relative, resolve } from "node:path"
11
+ import {
12
+ BACKEND_ONLY_MARKER,
13
+ browserDenial,
14
+ type Classification,
15
+ createZoneClassifier,
16
+ importAllowed,
17
+ importRuleMessage,
18
+ specifierPackage,
19
+ type Zone,
20
+ type ZoneClassifier,
21
+ } from "@nifrajs/web/zones"
22
+ import type * as ts from "typescript"
23
+
24
+ /** The code every zone diagnostic carries; TypeScript requires a number, and none of its own is this. */
25
+ export const ZONE_DIAGNOSTIC_CODE = 990028
26
+
27
+ /** Zones whose own code runs in a browser. */
28
+ const BROWSER_CODE: ReadonlySet<Zone> = new Set(["route-frontend", "frontend", "shared"])
29
+
30
+ interface ImportEdge {
31
+ readonly specifier: string
32
+ readonly literal: ts.StringLiteralLike
33
+ }
34
+
35
+ const allTypes = (elements: ts.NodeArray<ts.ImportSpecifier | ts.ExportSpecifier>): boolean =>
36
+ elements.length > 0 && elements.every((element) => element.isTypeOnly)
37
+
38
+ /** Every value import in a file: static, re-export and literal dynamic `import()`. Type-only ones are
39
+ * erased before bundling, so they may cross zones. */
40
+ function valueImports(tsm: typeof ts, source: ts.SourceFile): ImportEdge[] {
41
+ const edges: ImportEdge[] = []
42
+ const add = (node: ts.Expression | undefined): void => {
43
+ if (node !== undefined && tsm.isStringLiteralLike(node)) {
44
+ edges.push({ specifier: node.text, literal: node })
45
+ }
46
+ }
47
+ const visit = (node: ts.Node): void => {
48
+ if (tsm.isImportDeclaration(node)) {
49
+ const clause = node.importClause
50
+ const bindings = clause?.namedBindings
51
+ const typeOnly =
52
+ clause !== undefined &&
53
+ (clause.isTypeOnly ||
54
+ (clause.name === undefined &&
55
+ bindings !== undefined &&
56
+ tsm.isNamedImports(bindings) &&
57
+ allTypes(bindings.elements)))
58
+ if (!typeOnly) add(node.moduleSpecifier)
59
+ return
60
+ }
61
+ if (tsm.isExportDeclaration(node)) {
62
+ const clause = node.exportClause
63
+ const typeOnly =
64
+ node.isTypeOnly ||
65
+ (clause !== undefined && tsm.isNamedExports(clause) && allTypes(clause.elements))
66
+ if (!typeOnly) add(node.moduleSpecifier)
67
+ return
68
+ }
69
+ if (
70
+ tsm.isImportEqualsDeclaration(node) &&
71
+ !node.isTypeOnly &&
72
+ tsm.isExternalModuleReference(node.moduleReference)
73
+ ) {
74
+ add(node.moduleReference.expression)
75
+ return
76
+ }
77
+ if (tsm.isCallExpression(node) && node.expression.kind === tsm.SyntaxKind.ImportKeyword) {
78
+ add(node.arguments[0])
79
+ }
80
+ tsm.forEachChild(node, visit)
81
+ }
82
+ visit(source)
83
+ return edges
84
+ }
85
+
86
+ /** Where an import lands: TypeScript's resolution (tsconfig `paths` included), else a relative path
87
+ * that exists on disk (an `.svelte` or `.vue` file TypeScript only knows through a shim). */
88
+ function resolveImport(
89
+ tsm: typeof ts,
90
+ source: ts.SourceFile,
91
+ edge: ImportEdge,
92
+ options: ts.CompilerOptions,
93
+ ): string | undefined {
94
+ const specifier = edge.specifier.replace(/[?#].*$/, "")
95
+ const mode = tsm.getModeForUsageLocation(source, edge.literal, options)
96
+ const resolved = tsm.resolveModuleName(
97
+ specifier,
98
+ source.fileName,
99
+ options,
100
+ tsm.sys,
101
+ undefined,
102
+ undefined,
103
+ mode,
104
+ ).resolvedModule?.resolvedFileName
105
+ if (resolved !== undefined) return resolved
106
+ if (!specifier.startsWith("./") && !specifier.startsWith("../")) return undefined
107
+ const file = resolve(dirname(source.fileName), specifier)
108
+ return existsSync(file) ? file : undefined
109
+ }
110
+
111
+ const real = (file: string): string => {
112
+ try {
113
+ return realpathSync.native(file)
114
+ } catch {
115
+ return file
116
+ }
117
+ }
118
+
119
+ /** A third-party target as the specifier names it: TypeScript resolves `pg` to `@types/pg`, and the
120
+ * server-package list knows `pg`. */
121
+ function libraryAsImported(target: Classification, specifier: string): Classification {
122
+ if (target.zone !== "library") return target
123
+ const named = specifierPackage(specifier)
124
+ if (named === undefined || named === target.packageName) return target
125
+ return { zone: "library", packageName: named }
126
+ }
127
+
128
+ /** Why one import breaks the zone rules, or `undefined` when it does not. */
129
+ function edgeProblem(
130
+ classifier: ZoneClassifier,
131
+ display: (file: string) => string,
132
+ fileName: string,
133
+ from: Zone,
134
+ specifier: string,
135
+ target: string | undefined,
136
+ ): string | undefined {
137
+ const browser = BROWSER_CODE.has(from)
138
+ if (browser && /^(?:node|bun):/.test(specifier)) {
139
+ return `"${specifier}" may not reach the browser: Node and Bun built-ins run on the server only; use them in a route's backend half or under backend/`
140
+ }
141
+ if (browser && specifier === BACKEND_ONLY_MARKER) {
142
+ return `"${BACKEND_ONLY_MARKER}" marks a module backend-only, but ${display(fileName)} reaches the browser. Move it under backend/ or into a route's backend half`
143
+ }
144
+ if (target === undefined) return undefined
145
+ const to = classifier.classify(target)
146
+ if (to.zone === "error") return `${display(fileName)} imports ${display(target)}: ${to.reason}`
147
+ if (to.zone === "library") {
148
+ const reason = browser ? browserDenial(libraryAsImported(to, specifier), specifier) : undefined
149
+ return reason === undefined ? undefined : `"${specifier}" may not reach the browser: ${reason}`
150
+ }
151
+ return importAllowed(from, to.zone)
152
+ ? undefined
153
+ : importRuleMessage(display(fileName), from, display(target), to.zone)
154
+ }
155
+
156
+ /**
157
+ * The zone diagnostics for one file of the app rooted at `appRoot`. A fresh classifier per call keeps a
158
+ * moved file or an edited package declaration from going stale; it costs a few `stat`s per import.
159
+ */
160
+ export function zoneDiagnostics(
161
+ tsm: typeof ts,
162
+ source: ts.SourceFile,
163
+ appRoot: string,
164
+ options: ts.CompilerOptions,
165
+ ): ts.Diagnostic[] {
166
+ const classifier = createZoneClassifier({ appRoot })
167
+ const from = classifier.classify(source.fileName)
168
+ if (from.zone === "error" || from.zone === "library" || from.zone === "generated") return []
169
+ const display = (file: string): string => {
170
+ const rel = relative(classifier.appRoot, real(file)).replaceAll("\\", "/")
171
+ return rel.startsWith("../") || isAbsolute(rel) ? file : rel
172
+ }
173
+ const diagnostics: ts.Diagnostic[] = []
174
+ for (const edge of valueImports(tsm, source)) {
175
+ const target = resolveImport(tsm, source, edge, options)
176
+ const message = edgeProblem(
177
+ classifier,
178
+ display,
179
+ source.fileName,
180
+ from.zone,
181
+ edge.specifier,
182
+ target,
183
+ )
184
+ if (message === undefined) continue
185
+ diagnostics.push({
186
+ file: source,
187
+ start: edge.literal.getStart(source),
188
+ length: edge.literal.getWidth(source),
189
+ messageText: message,
190
+ category: tsm.DiagnosticCategory.Error,
191
+ code: ZONE_DIAGNOSTIC_CODE,
192
+ source: "nifra",
193
+ })
194
+ }
195
+ return diagnostics
196
+ }