vite-plugin-nudo 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # vite-plugin-nudo
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Conceptual design and basic implementation.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies
12
+ - @nudojs/service@0.1.0
@@ -0,0 +1,8 @@
1
+ type NudoPluginOptions = {
2
+ include?: string[];
3
+ exclude?: string[];
4
+ failOnError?: boolean;
5
+ };
6
+ declare function nudoPlugin(options?: NudoPluginOptions): any;
7
+
8
+ export { type NudoPluginOptions, nudoPlugin as default };
package/dist/index.js ADDED
@@ -0,0 +1,64 @@
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));
9
+ }
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));
15
+ }
16
+ return str.includes(pattern);
17
+ }
18
+ function nudoPlugin(options = {}) {
19
+ const include = options.include ?? DEFAULT_INCLUDE;
20
+ const exclude = options.exclude ?? DEFAULT_EXCLUDE;
21
+ const failOnError = options.failOnError ?? false;
22
+ const analysisCache = /* @__PURE__ */ new Map();
23
+ return {
24
+ name: "vite-plugin-nudo",
25
+ buildStart() {
26
+ analysisCache.clear();
27
+ },
28
+ transform(code, id) {
29
+ if (!matchesPatterns(id, include, exclude)) return null;
30
+ if (!/@nudo:(case|mock|pure|skip|sample|returns)\b/.test(code)) return null;
31
+ try {
32
+ const result = analyzeFile(id, code);
33
+ analysisCache.set(id, result);
34
+ for (const diag of result.diagnostics) {
35
+ const loc = `${id}:${diag.range.start.line}:${diag.range.start.column}`;
36
+ const msg = `[nudo] ${loc} ${diag.severity}: ${diag.message}`;
37
+ if (diag.severity === "error") {
38
+ if (failOnError) {
39
+ this.error(msg);
40
+ } else {
41
+ this.warn(msg);
42
+ }
43
+ } else if (diag.severity === "warning") {
44
+ this.warn(msg);
45
+ }
46
+ }
47
+ } catch (err) {
48
+ this.warn(`[nudo] Failed to analyze ${id}: ${err.message}`);
49
+ }
50
+ return null;
51
+ },
52
+ buildEnd() {
53
+ const totalDiags = Array.from(analysisCache.values()).reduce((sum, r) => sum + r.diagnostics.length, 0);
54
+ if (totalDiags > 0) {
55
+ const errorCount = Array.from(analysisCache.values()).reduce((sum, r) => sum + r.diagnostics.filter((d) => d.severity === "error").length, 0);
56
+ const warnCount = totalDiags - errorCount;
57
+ console.log(`[nudo] Analysis complete: ${errorCount} error(s), ${warnCount} warning(s)`);
58
+ }
59
+ }
60
+ };
61
+ }
62
+ export {
63
+ nudoPlugin as default
64
+ };
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "vite-plugin-nudo",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "src/index.ts",
6
+ "dependencies": {
7
+ "@nudojs/service": "0.1.0"
8
+ },
9
+ "peerDependencies": {
10
+ "vite": ">=5.0.0"
11
+ },
12
+ "scripts": {
13
+ "build": "tsup src/index.ts --format esm --dts"
14
+ }
15
+ }
@@ -0,0 +1,81 @@
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 ADDED
@@ -0,0 +1,81 @@
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)\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 ADDED
@@ -0,0 +1,11 @@
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
+ }