vite-plugin-nudo 0.2.1 → 0.3.1

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/dist/index.js CHANGED
@@ -1,37 +1,105 @@
1
1
  // src/index.ts
2
- import { analyzeFile } from "@nudojs/service";
3
- var DEFAULT_INCLUDE = ["**/*.js"];
4
- var DEFAULT_EXCLUDE = ["**/node_modules/**"];
5
- function matchesPatterns(id, include, exclude) {
6
- const isExcluded = exclude.some((p) => minimatch(id, p));
7
- if (isExcluded) return false;
8
- return include.some((p) => minimatch(id, p));
2
+ import {
3
+ analyzeFileAsync,
4
+ defaultLoadModule as loadModule,
5
+ clearAnalysisSessionCaches
6
+ } from "@nudojs/service";
7
+ import { checkSource, pTrue } from "@nudojs/core";
8
+ var NUDO_DIRECTIVE_RE = /@nudo:(case|mock|pure|skip|sample|refine|import|env|mock-module|as|replace)\b/;
9
+ function checkIssuesToDiagnostics(id, code) {
10
+ try {
11
+ const report = checkSource(id, code, pTrue, { loadModule, fromFile: id });
12
+ return report.issues.filter((i) => i.severity === "error" || i.severity === "warning").map((i) => {
13
+ const line = i.line ?? 1;
14
+ const column = i.column ?? 0;
15
+ const parts = [i.message];
16
+ if (i.actual) parts.push(`actual: ${i.actual}`);
17
+ if (i.expected) parts.push(`expected: ${i.expected}`);
18
+ if (i.suggestion) parts.push(i.suggestion);
19
+ if (i.code) parts.push(`(${i.code})`);
20
+ return {
21
+ severity: i.severity === "error" ? "error" : "warning",
22
+ message: parts.join(" \xB7 "),
23
+ code: i.code,
24
+ range: {
25
+ start: { line, column },
26
+ end: { line, column: column + 1 }
27
+ }
28
+ };
29
+ });
30
+ } catch {
31
+ return [];
32
+ }
33
+ }
34
+ var DEFAULT_INCLUDE = ["**/*.js", "**/*.mjs", "**/*.ts", "**/*.mts"];
35
+ var DEFAULT_EXCLUDE = ["**/node_modules/**", "**/*.d.ts"];
36
+ var REGEX_SPECIALS = /[\\^$.|?*+(){}\[\]]/;
37
+ function escapeRegExpChar(ch) {
38
+ return REGEX_SPECIALS.test(ch) ? `\\${ch}` : ch;
9
39
  }
10
- function minimatch(str, pattern) {
11
- if (pattern === "**/*.js") return str.endsWith(".js");
12
- if (pattern === "**/node_modules/**") return str.includes("/node_modules/");
13
- if (pattern.startsWith("**/")) {
14
- return str.endsWith(pattern.slice(3)) || str.includes(pattern.slice(2));
40
+ function segmentToRegExpSource(segment) {
41
+ let source = "";
42
+ for (const ch of segment) {
43
+ if (ch === "*") source += "[^/]*";
44
+ else if (ch === "?") source += "[^/]";
45
+ else source += escapeRegExpChar(ch);
15
46
  }
16
- return str.includes(pattern);
47
+ return source;
48
+ }
49
+ function patternToRegExp(pattern) {
50
+ const segments = pattern.split("/");
51
+ let source = "";
52
+ for (let i = 0; i < segments.length; i++) {
53
+ const segment = segments[i];
54
+ const isLast = i === segments.length - 1;
55
+ if (segment === "**") {
56
+ if (isLast) {
57
+ source = source.endsWith("/") ? `${source.slice(0, -1)}(?:/.*)?` : `${source}.*`;
58
+ } else {
59
+ source += "(?:.*/)?";
60
+ }
61
+ } else {
62
+ source += segmentToRegExpSource(segment);
63
+ if (!isLast) source += "/";
64
+ }
65
+ }
66
+ return new RegExp(`^${source}$`);
67
+ }
68
+ function compilePattern(pattern) {
69
+ if (!/[*?]/.test(pattern)) return (id) => id.includes(pattern);
70
+ const regExp = patternToRegExp(pattern);
71
+ return (id) => regExp.test(id);
72
+ }
73
+ function compileAnyMatcher(patterns) {
74
+ const matchers = patterns.map(compilePattern);
75
+ return (id) => matchers.some((match) => match(id));
17
76
  }
18
77
  function nudoPlugin(options = {}) {
19
- const include = options.include ?? DEFAULT_INCLUDE;
20
- const exclude = options.exclude ?? DEFAULT_EXCLUDE;
78
+ const includeMatch = compileAnyMatcher(options.include ?? DEFAULT_INCLUDE);
79
+ const excludeMatch = compileAnyMatcher(options.exclude ?? DEFAULT_EXCLUDE);
21
80
  const failOnError = options.failOnError ?? false;
22
81
  const analysisCache = /* @__PURE__ */ new Map();
23
82
  return {
24
83
  name: "vite-plugin-nudo",
25
84
  buildStart() {
26
85
  analysisCache.clear();
86
+ clearAnalysisSessionCaches();
87
+ },
88
+ /** 任一文件变更后,未改 source 的 importer 再 transform 时可能命中陈旧会话缓存 */
89
+ watchChange() {
90
+ analysisCache.clear();
91
+ clearAnalysisSessionCaches();
27
92
  },
28
- transform(code, id) {
29
- if (!matchesPatterns(id, include, exclude)) return null;
30
- if (!/@nudo:(case|mock|pure|skip|sample|returns|env|mock-module|as|replace)\b/.test(code)) return null;
93
+ async transform(code, id) {
94
+ if (excludeMatch(id)) return null;
95
+ if (!includeMatch(id)) return null;
96
+ if (!NUDO_DIRECTIVE_RE.test(code)) return null;
31
97
  try {
32
- const result = analyzeFile(id, code);
33
- analysisCache.set(id, result);
34
- for (const diag of result.diagnostics) {
98
+ const result = await analyzeFileAsync(id, code);
99
+ const checkDiags = checkIssuesToDiagnostics(id, code);
100
+ const merged = { ...result, diagnostics: [...result.diagnostics, ...checkDiags] };
101
+ analysisCache.set(id, merged);
102
+ for (const diag of merged.diagnostics) {
35
103
  const loc = `${id}:${diag.range.start.line}:${diag.range.start.column}`;
36
104
  const msg = `[nudo] ${loc} ${diag.severity}: ${diag.message}`;
37
105
  if (diag.severity === "error") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-nudo",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Vite plugin for build-time JavaScript type inference with Nudo",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -11,7 +11,17 @@
11
11
  "vite",
12
12
  "vite-plugin"
13
13
  ],
14
- "main": "src/index.ts",
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
15
25
  "repository": {
16
26
  "type": "git",
17
27
  "url": "git+https://github.com/nudojs/nudo.git",
@@ -25,7 +35,8 @@
25
35
  "registry": "https://registry.npmjs.org"
26
36
  },
27
37
  "dependencies": {
28
- "@nudojs/service": "0.2.1"
38
+ "@nudojs/core": "0.3.1",
39
+ "@nudojs/service": "0.3.1"
29
40
  },
30
41
  "peerDependencies": {
31
42
  "vite": ">=5.0.0"
package/CHANGELOG.md DELETED
@@ -1,33 +0,0 @@
1
- # vite-plugin-nudo
2
-
3
- ## 0.2.1
4
-
5
- ### Patch Changes
6
-
7
- - @nudojs/service@0.2.1
8
-
9
- ## 0.2.0
10
-
11
- ### Minor Changes
12
-
13
- - 6c38283: docs and ci
14
- - 9f7f819: fix pkg info
15
- - c175f71: version
16
-
17
- ### Patch Changes
18
-
19
- - Updated dependencies [6c38283]
20
- - Updated dependencies [9f7f819]
21
- - Updated dependencies [c175f71]
22
- - @nudojs/service@0.2.0
23
-
24
- ## 0.1.0
25
-
26
- ### Minor Changes
27
-
28
- - Conceptual design and basic implementation.
29
-
30
- ### Patch Changes
31
-
32
- - Updated dependencies
33
- - @nudojs/service@0.1.0
@@ -1,81 +0,0 @@
1
- import { describe, it, expect, vi } from "vitest";
2
- import nudoPlugin from "../index.ts";
3
-
4
- describe("vite-plugin-nudo", () => {
5
- it("creates a plugin with correct name", () => {
6
- const plugin = nudoPlugin();
7
- expect(plugin.name).toBe("vite-plugin-nudo");
8
- });
9
-
10
- it("returns null for files without @nudo: directives", () => {
11
- const plugin = nudoPlugin();
12
- const result = plugin.transform.call({}, "const x = 1;", "/test/file.js");
13
- expect(result).toBeNull();
14
- });
15
-
16
- it("returns null for node_modules files", () => {
17
- const plugin = nudoPlugin();
18
- const result = plugin.transform.call({}, "const x = 1;", "/node_modules/pkg/file.js");
19
- expect(result).toBeNull();
20
- });
21
-
22
- it("analyzes files with @nudo: directives and reports warnings", () => {
23
- const plugin = nudoPlugin();
24
- const warnFn = vi.fn();
25
- const ctx = { warn: warnFn, error: vi.fn() };
26
-
27
- const source = `
28
- /**
29
- * @nudo:case "negative" (-1)
30
- */
31
- function safeSqrt(x) {
32
- if (x < 0) {
33
- throw new RangeError("negative input");
34
- }
35
- return x;
36
- }
37
- `;
38
- const result = plugin.transform.call(ctx, source, "/test/throws.js");
39
- expect(result).toBeNull();
40
- expect(warnFn).toHaveBeenCalled();
41
- });
42
-
43
- it("respects custom include patterns", () => {
44
- const plugin = nudoPlugin({ include: ["**/*.typed.js"] });
45
- const result = plugin.transform.call({}, "const x = 1;", "/test/file.js");
46
- expect(result).toBeNull();
47
- });
48
-
49
- it("clears cache on buildStart", () => {
50
- const plugin = nudoPlugin();
51
- plugin.buildStart.call({});
52
- // No error means cache cleared successfully
53
- });
54
-
55
- it("reports summary on buildEnd", () => {
56
- const plugin = nudoPlugin();
57
- const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
58
- plugin.buildStart.call({});
59
- plugin.buildEnd.call({});
60
- consoleSpy.mockRestore();
61
- });
62
-
63
- it("errors when failOnError is true and diagnostics have errors", () => {
64
- const plugin = nudoPlugin({ failOnError: true });
65
- const errorFn = vi.fn();
66
- const warnFn = vi.fn();
67
- const ctx = { warn: warnFn, error: errorFn };
68
-
69
- const source = `
70
- /**
71
- * @nudo:case "test" (T.number)
72
- * @nudo:returns (T.string)
73
- */
74
- function identity(x) {
75
- return x;
76
- }
77
- `;
78
- plugin.transform.call(ctx, source, "/test/fail.js");
79
- expect(errorFn).toHaveBeenCalled();
80
- });
81
- });
package/src/index.ts DELETED
@@ -1,81 +0,0 @@
1
- import { analyzeFile, type AnalysisResult } from "@nudojs/service";
2
-
3
- export type NudoPluginOptions = {
4
- include?: string[];
5
- exclude?: string[];
6
- failOnError?: boolean;
7
- };
8
-
9
- const DEFAULT_INCLUDE = ["**/*.js"];
10
- const DEFAULT_EXCLUDE = ["**/node_modules/**"];
11
-
12
- function matchesPatterns(id: string, include: string[], exclude: string[]): boolean {
13
- const isExcluded = exclude.some((p) => minimatch(id, p));
14
- if (isExcluded) return false;
15
- return include.some((p) => minimatch(id, p));
16
- }
17
-
18
- function minimatch(str: string, pattern: string): boolean {
19
- if (pattern === "**/*.js") return str.endsWith(".js");
20
- if (pattern === "**/node_modules/**") return str.includes("/node_modules/");
21
- if (pattern.startsWith("**/")) {
22
- return str.endsWith(pattern.slice(3)) || str.includes(pattern.slice(2));
23
- }
24
- return str.includes(pattern);
25
- }
26
-
27
- export default function nudoPlugin(options: NudoPluginOptions = {}): any {
28
- const include = options.include ?? DEFAULT_INCLUDE;
29
- const exclude = options.exclude ?? DEFAULT_EXCLUDE;
30
- const failOnError = options.failOnError ?? false;
31
-
32
- const analysisCache = new Map<string, AnalysisResult>();
33
-
34
- return {
35
- name: "vite-plugin-nudo",
36
-
37
- buildStart() {
38
- analysisCache.clear();
39
- },
40
-
41
- transform(code: string, id: string) {
42
- if (!matchesPatterns(id, include, exclude)) return null;
43
- if (!/@nudo:(case|mock|pure|skip|sample|returns|env|mock-module|as|replace)\b/.test(code)) return null;
44
-
45
- try {
46
- const result = analyzeFile(id, code);
47
- analysisCache.set(id, result);
48
-
49
- for (const diag of result.diagnostics) {
50
- const loc = `${id}:${diag.range.start.line}:${diag.range.start.column}`;
51
- const msg = `[nudo] ${loc} ${diag.severity}: ${diag.message}`;
52
-
53
- if (diag.severity === "error") {
54
- if (failOnError) {
55
- (this as any).error(msg);
56
- } else {
57
- (this as any).warn(msg);
58
- }
59
- } else if (diag.severity === "warning") {
60
- (this as any).warn(msg);
61
- }
62
- }
63
- } catch (err) {
64
- (this as any).warn(`[nudo] Failed to analyze ${id}: ${(err as Error).message}`);
65
- }
66
-
67
- return null;
68
- },
69
-
70
- buildEnd() {
71
- const totalDiags = Array.from(analysisCache.values())
72
- .reduce((sum, r) => sum + r.diagnostics.length, 0);
73
- if (totalDiags > 0) {
74
- const errorCount = Array.from(analysisCache.values())
75
- .reduce((sum, r) => sum + r.diagnostics.filter((d) => d.severity === "error").length, 0);
76
- const warnCount = totalDiags - errorCount;
77
- console.log(`[nudo] Analysis complete: ${errorCount} error(s), ${warnCount} warning(s)`);
78
- }
79
- },
80
- };
81
- }
package/tsconfig.json DELETED
@@ -1,11 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src"
6
- },
7
- "include": ["src"],
8
- "references": [
9
- { "path": "../service" }
10
- ]
11
- }