nginx-lint-plugin 0.0.1 → 0.8.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
@@ -1,45 +1,256 @@
1
1
  # nginx-lint-plugin
2
2
 
3
- ## ⚠️ IMPORTANT NOTICE ⚠️
3
+ TypeScript SDK for writing [nginx-lint](https://github.com/walf443/nginx-lint) plugins.
4
4
 
5
- **This package is created solely for the purpose of setting up OIDC (OpenID Connect) trusted publishing with npm.**
5
+ Plugins are compiled to WebAssembly Component Model modules and loaded by the nginx-lint CLI at runtime.
6
6
 
7
- This is **NOT** a functional package and contains **NO** code or functionality beyond the OIDC setup configuration.
7
+ ## Install
8
8
 
9
- ## Purpose
9
+ ```bash
10
+ npm install nginx-lint-plugin
11
+ ```
10
12
 
11
- This package exists to:
12
- 1. Configure OIDC trusted publishing for the package name `nginx-lint-plugin`
13
- 2. Enable secure, token-less publishing from CI/CD workflows
14
- 3. Establish provenance for packages published under this name
13
+ ## Quick Start
15
14
 
16
- ## What is OIDC Trusted Publishing?
15
+ A plugin exports two functions: `spec` (metadata) and `check` (lint logic).
17
16
 
18
- OIDC trusted publishing allows package maintainers to publish packages directly from their CI/CD workflows without needing to manage npm access tokens. Instead, it uses OpenID Connect to establish trust between the CI/CD provider (like GitHub Actions) and npm.
17
+ ```typescript
18
+ // src/plugin.ts
19
+ import type { Config, LintError, PluginSpec } from "nginx-lint-plugin";
19
20
 
20
- ## Setup Instructions
21
+ export function spec(): PluginSpec {
22
+ return {
23
+ name: "my-rule",
24
+ category: "best-practices",
25
+ description: "Describe what this rule checks",
26
+ apiVersion: "1.0",
27
+ severity: "warning",
28
+ };
29
+ }
21
30
 
22
- To properly configure OIDC trusted publishing for this package:
31
+ export function check(cfg: Config, path: string): LintError[] {
32
+ const errors: LintError[] = [];
23
33
 
24
- 1. Go to [npmjs.com](https://www.npmjs.com/) and navigate to your package settings
25
- 2. Configure the trusted publisher (e.g., GitHub Actions)
26
- 3. Specify the repository and workflow that should be allowed to publish
27
- 4. Use the configured workflow to publish your actual package
34
+ for (const ctx of cfg.allDirectivesWithContext()) {
35
+ const directive = ctx.directive;
28
36
 
29
- ## DO NOT USE THIS PACKAGE
37
+ if (directive.is("proxy_pass") && !directive.hasBlock()) {
38
+ errors.push({
39
+ rule: "my-rule",
40
+ category: "best-practices",
41
+ message: "proxy_pass should ...",
42
+ severity: "warning",
43
+ line: directive.line(),
44
+ column: directive.column(),
45
+ fixes: [directive.replaceWith("proxy_pass http://upstream;")],
46
+ });
47
+ }
48
+ }
30
49
 
31
- This package is a placeholder for OIDC configuration only. It:
32
- - Contains no executable code
33
- - Provides no functionality
34
- - Should not be installed as a dependency
35
- - Exists only for administrative purposes
50
+ return errors;
51
+ }
52
+ ```
36
53
 
37
- ## More Information
54
+ ## Testing
38
55
 
39
- For more details about npm's trusted publishing feature, see:
40
- - [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
41
- - [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
56
+ The `nginx-lint-plugin/testing` entry provides parser-based testing utilities. Tests parse real nginx configuration strings using the same Rust parser that powers the production linter.
42
57
 
43
- ---
58
+ ```typescript
59
+ import { describe, it } from "node:test";
60
+ import { spec, check } from "./plugin.js";
61
+ import { parseConfig, PluginTestRunner } from "nginx-lint-plugin/testing";
44
62
 
45
- **Maintained for OIDC setup purposes only**
63
+ describe("my-rule", () => {
64
+ const runner = new PluginTestRunner(spec, check);
65
+
66
+ it("detects the issue", () => {
67
+ runner.assertErrors("http {\n proxy_pass http://bad;\n}", 1);
68
+ });
69
+
70
+ it("passes valid config", () => {
71
+ runner.assertErrors("http {\n proxy_pass http://good;\n}", 0);
72
+ });
73
+
74
+ it("checks error on specific line", () => {
75
+ runner.assertErrorOnLine("http {\n proxy_pass http://bad;\n}", 2);
76
+ });
77
+
78
+ it("validates bad/good examples", () => {
79
+ runner.testExamples(
80
+ "http {\n proxy_pass http://bad;\n}",
81
+ "http {\n proxy_pass http://good;\n}",
82
+ );
83
+ });
84
+ });
85
+ ```
86
+
87
+ ### Testing with include context
88
+
89
+ Use `parseConfig` directly to simulate files included from specific blocks:
90
+
91
+ ```typescript
92
+ it("handles included files", () => {
93
+ const cfg = parseConfig("server_tokens off;", {
94
+ includeContext: ["http", "server"],
95
+ });
96
+ const errors = check(cfg, "test.conf");
97
+ assert.equal(errors.length, 0);
98
+ });
99
+ ```
100
+
101
+ ### PluginTestRunner
102
+
103
+ | Method | Description |
104
+ |--------|-------------|
105
+ | `checkString(content, opts?)` | Parse and check, returning errors from this rule |
106
+ | `assertErrors(content, count)` | Assert exactly N errors |
107
+ | `assertErrorOnLine(content, line)` | Assert error on a specific line |
108
+ | `testExamples(badConf, goodConf)` | Validate bad config produces errors, good config does not |
109
+
110
+ ## Building a Plugin
111
+
112
+ ### package.json
113
+
114
+ The WIT definition file is bundled with this package, so `jco componentize` can reference it directly from `node_modules`.
115
+
116
+ ```json
117
+ {
118
+ "name": "my-plugin",
119
+ "type": "module",
120
+ "scripts": {
121
+ "build": "tsc && jco componentize dist/plugin.js -w node_modules/nginx-lint-plugin/wit -n plugin --disable all -o dist/my-plugin.wasm",
122
+ "test": "tsc && node --test dist/plugin.test.js"
123
+ },
124
+ "dependencies": {
125
+ "nginx-lint-plugin": "^0.8.2"
126
+ },
127
+ "devDependencies": {
128
+ "@bytecodealliance/componentize-js": "^0.19",
129
+ "@bytecodealliance/jco": "^1",
130
+ "typescript": "^5"
131
+ }
132
+ }
133
+ ```
134
+
135
+ ### tsconfig.json
136
+
137
+ ```json
138
+ {
139
+ "compilerOptions": {
140
+ "target": "ES2022",
141
+ "module": "ES2022",
142
+ "moduleResolution": "bundler",
143
+ "outDir": "dist",
144
+ "strict": true,
145
+ "skipLibCheck": true,
146
+ "declaration": true
147
+ },
148
+ "include": ["src"]
149
+ }
150
+ ```
151
+
152
+ ### Build and run
153
+
154
+ ```bash
155
+ npm run build
156
+ nginx-lint --plugins ./dist path/to/nginx.conf
157
+ ```
158
+
159
+ The `--plugins` option takes a directory path. nginx-lint automatically loads all `.wasm` files found in that directory.
160
+
161
+ ## API Reference
162
+
163
+ ### Types
164
+
165
+ ```typescript
166
+ import type {
167
+ // Core types
168
+ Severity, // "error" | "warning"
169
+ Fix, // Autofix descriptor
170
+ LintError, // Lint error with rule, message, line, column, fixes
171
+ PluginSpec, // Plugin metadata
172
+
173
+ // Directive data
174
+ ArgumentType, // "literal" | "quoted-string" | "single-quoted-string" | "variable"
175
+ ArgumentInfo, // Argument with value, raw text, type, position
176
+ DirectiveData, // Flat directive properties
177
+
178
+ // Config tree
179
+ Config, // Parsed nginx configuration
180
+ Directive, // A single directive with methods
181
+ DirectiveContext,// Directive with parent stack and depth
182
+ ConfigItem, // Directive | Comment | BlankLine
183
+ } from "nginx-lint-plugin";
184
+ ```
185
+
186
+ ### Config
187
+
188
+ | Method | Description |
189
+ |--------|-------------|
190
+ | `allDirectivesWithContext()` | All directives with parent context (DFS order) |
191
+ | `allDirectives()` | All directives without context |
192
+ | `items()` | Top-level config items (directives, comments, blank lines) |
193
+ | `includeContext()` | Parent block names from `include` directives |
194
+ | `isIncludedFrom(context)` | Check if included from a specific block |
195
+ | `isIncludedFromHttp()` | Check if included from `http` block |
196
+ | `isIncludedFromHttpServer()` | Check if included from `http > server` |
197
+ | `isIncludedFromHttpLocation()` | Check if included from `http > ... > location` |
198
+ | `isIncludedFromStream()` | Check if included from `stream` block |
199
+ | `immediateParentContext()` | Immediate parent block name |
200
+
201
+ ### Directive
202
+
203
+ | Method | Description |
204
+ |--------|-------------|
205
+ | `name()` | Directive name (e.g. `"server_tokens"`) |
206
+ | `is(name)` | Check directive name |
207
+ | `firstArg()` | First argument value |
208
+ | `firstArgIs(value)` | Check first argument |
209
+ | `argAt(index)` | Argument at index |
210
+ | `lastArg()` | Last argument value |
211
+ | `hasArg(value)` | Check if argument exists |
212
+ | `argCount()` | Number of arguments |
213
+ | `args()` | All arguments as `ArgumentInfo[]` |
214
+ | `line()` / `column()` | Source position |
215
+ | `hasBlock()` | Whether directive has a `{ }` block |
216
+ | `blockItems()` | Child items inside the block |
217
+ | `blockIsRaw()` | Whether block content is raw (e.g. `map`) |
218
+
219
+ ### Directive Fix Builders
220
+
221
+ | Method | Description |
222
+ |--------|-------------|
223
+ | `replaceWith(newText)` | Replace the entire directive |
224
+ | `deleteLineFix()` | Delete the directive's line |
225
+ | `insertAfter(newText)` | Insert text after the directive |
226
+ | `insertBefore(newText)` | Insert text before the directive |
227
+ | `insertAfterMany(lines)` | Insert multiple lines after |
228
+ | `insertBeforeMany(lines)` | Insert multiple lines before |
229
+
230
+ ### DirectiveContext
231
+
232
+ | Field | Description |
233
+ |-------|-------------|
234
+ | `directive` | The directive |
235
+ | `parentStack` | Parent block names (e.g. `["http", "server"]`) |
236
+ | `depth` | Nesting depth |
237
+
238
+ ### PluginSpec
239
+
240
+ ```typescript
241
+ {
242
+ name: string; // Rule identifier (e.g. "my-rule")
243
+ category: string; // Category (e.g. "security", "best-practices", "style", "syntax")
244
+ description: string; // Human-readable description
245
+ apiVersion: string; // API version ("1.0")
246
+ severity?: string; // Default: "warning". Also accepts "error"
247
+ why?: string; // Explanation of why this rule matters
248
+ badExample?: string; // Config that triggers the rule
249
+ goodExample?: string; // Config that passes the rule
250
+ references?: string[];// Links to relevant documentation
251
+ }
252
+ ```
253
+
254
+ ## License
255
+
256
+ MIT
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Builds WIT-compatible Config/Directive objects from parser component output.
3
+ *
4
+ * The parser WASM component returns a ParseOutput with an index-based tree
5
+ * representation (to avoid recursive types in WIT). This module reconstructs
6
+ * the method-based Directive/Config interfaces from that flat representation.
7
+ */
8
+ import type { Config } from "./generated/interfaces/nginx-lint-plugin-config-api.js";
9
+ import type { ParseOutput } from "../wasm/parser/interfaces/nginx-lint-plugin-parser-types.js";
10
+ export declare function buildConfigFromParseOutput(output: ParseOutput): Config;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Builds WIT-compatible Config/Directive objects from parser component output.
3
+ *
4
+ * The parser WASM component returns a ParseOutput with an index-based tree
5
+ * representation (to avoid recursive types in WIT). This module reconstructs
6
+ * the method-based Directive/Config interfaces from that flat representation.
7
+ */
8
+ import { makeIncludeContextMethods } from "./include-context.js";
9
+ // ── Wrap DirectiveData with Directive interface ─────────────────────
10
+ function wrapDirective(data, resolveBlockItems) {
11
+ const argValues = data.args.map((a) => a.value);
12
+ return {
13
+ data() { return data; },
14
+ name() { return data.name; },
15
+ is(name) { return data.name === name; },
16
+ firstArg() { return argValues[0] ?? undefined; },
17
+ firstArgIs(value) { return argValues[0] === value; },
18
+ argAt(index) { return argValues[index] ?? undefined; },
19
+ lastArg() { return argValues.length > 0 ? argValues[argValues.length - 1] : undefined; },
20
+ hasArg(value) { return argValues.includes(value); },
21
+ argCount() { return argValues.length; },
22
+ args() { return data.args; },
23
+ line() { return data.line; },
24
+ column() { return data.column; },
25
+ startOffset() { return data.startOffset; },
26
+ endOffset() { return data.endOffset; },
27
+ leadingWhitespace() { return data.leadingWhitespace; },
28
+ trailingWhitespace() { return data.trailingWhitespace; },
29
+ spaceBeforeTerminator() { return data.spaceBeforeTerminator; },
30
+ hasBlock() { return data.hasBlock; },
31
+ blockItems() { return resolveBlockItems(); },
32
+ blockIsRaw() { return data.blockIsRaw; },
33
+ replaceWith(newText) {
34
+ return {
35
+ line: data.line, oldText: undefined, newText,
36
+ deleteLine: false, insertAfter: false,
37
+ startOffset: data.startOffset, endOffset: data.endOffset,
38
+ };
39
+ },
40
+ deleteLineFix() {
41
+ return {
42
+ line: data.line, oldText: undefined, newText: "",
43
+ deleteLine: true, insertAfter: false,
44
+ startOffset: undefined, endOffset: undefined,
45
+ };
46
+ },
47
+ insertAfter(newText) {
48
+ return {
49
+ line: data.line, oldText: undefined, newText,
50
+ deleteLine: false, insertAfter: true,
51
+ startOffset: undefined, endOffset: undefined,
52
+ };
53
+ },
54
+ insertBefore(newText) {
55
+ return {
56
+ line: data.line, oldText: undefined, newText,
57
+ deleteLine: false, insertAfter: false,
58
+ startOffset: undefined, endOffset: undefined,
59
+ };
60
+ },
61
+ insertAfterMany(lines) {
62
+ return {
63
+ line: data.line, oldText: undefined, newText: lines.join("\n"),
64
+ deleteLine: false, insertAfter: true,
65
+ startOffset: undefined, endOffset: undefined,
66
+ };
67
+ },
68
+ insertBeforeMany(lines) {
69
+ return {
70
+ line: data.line, oldText: undefined, newText: lines.join("\n"),
71
+ deleteLine: false, insertAfter: false,
72
+ startOffset: undefined, endOffset: undefined,
73
+ };
74
+ },
75
+ };
76
+ }
77
+ // ── Resolve index-based items to ConfigItem tree ────────────────────
78
+ function resolveConfigItem(allItems, index) {
79
+ const item = allItems[index];
80
+ if (item.value.tag === "directive-item") {
81
+ const data = item.value.val;
82
+ const childIndices = item.childIndices;
83
+ return {
84
+ tag: "directive-item",
85
+ val: wrapDirective(data, () => Array.from(childIndices).map((i) => resolveConfigItem(allItems, i))),
86
+ };
87
+ }
88
+ if (item.value.tag === "comment-item") {
89
+ return { tag: "comment-item", val: item.value.val };
90
+ }
91
+ return { tag: "blank-line-item", val: item.value.val };
92
+ }
93
+ // ── Build Config from ParseOutput ───────────────────────────────────
94
+ export function buildConfigFromParseOutput(output) {
95
+ const inclCtx = output.includeContext;
96
+ const allItems = output.allItems;
97
+ const directiveContexts = output.directivesWithContext.map((ctx) => ({
98
+ directive: wrapDirective(ctx.data, () => Array.from(ctx.blockItemIndices).map((i) => resolveConfigItem(allItems, i))),
99
+ parentStack: ctx.parentStack,
100
+ depth: ctx.depth,
101
+ }));
102
+ const topLevelItems = Array.from(output.topLevelIndices).map((i) => resolveConfigItem(allItems, i));
103
+ return {
104
+ allDirectivesWithContext() { return directiveContexts; },
105
+ allDirectives() { return directiveContexts.map((c) => c.directive); },
106
+ items() { return topLevelItems; },
107
+ ...makeIncludeContextMethods(inclCtx),
108
+ };
109
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Shared include-context helpers for Config implementations.
3
+ *
4
+ * Provides the include-context portion of the Config interface so that
5
+ * config-builder.ts can reuse a single, tested implementation.
6
+ */
7
+ export interface IncludeContextMethods {
8
+ includeContext(): string[];
9
+ isIncludedFrom(context: string): boolean;
10
+ isIncludedFromHttp(): boolean;
11
+ isIncludedFromHttpServer(): boolean;
12
+ isIncludedFromHttpLocation(): boolean;
13
+ isIncludedFromStream(): boolean;
14
+ immediateParentContext(): string | undefined;
15
+ }
16
+ export declare function makeIncludeContextMethods(inclCtx: string[]): IncludeContextMethods;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Shared include-context helpers for Config implementations.
3
+ *
4
+ * Provides the include-context portion of the Config interface so that
5
+ * config-builder.ts can reuse a single, tested implementation.
6
+ */
7
+ export function makeIncludeContextMethods(inclCtx) {
8
+ return {
9
+ includeContext() { return inclCtx; },
10
+ isIncludedFrom(context) { return inclCtx.includes(context); },
11
+ isIncludedFromHttp() { return inclCtx.includes("http"); },
12
+ isIncludedFromHttpServer() {
13
+ const httpIndex = inclCtx.indexOf("http");
14
+ const serverIndex = inclCtx.indexOf("server");
15
+ return httpIndex !== -1 && serverIndex !== -1 && httpIndex < serverIndex;
16
+ },
17
+ isIncludedFromHttpLocation() {
18
+ const httpIndex = inclCtx.indexOf("http");
19
+ const locationIndex = inclCtx.indexOf("location");
20
+ return httpIndex !== -1 && locationIndex !== -1 && httpIndex < locationIndex;
21
+ },
22
+ isIncludedFromStream() { return inclCtx.includes("stream"); },
23
+ immediateParentContext() { return inclCtx.length > 0 ? inclCtx[inclCtx.length - 1] : undefined; },
24
+ };
25
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * nginx-lint-plugin — shared TypeScript library for nginx-lint WASM plugins.
3
+ *
4
+ * Types are auto-generated from the WIT definition (wit/nginx-lint-plugin.wit)
5
+ * by `jco types` during the build step.
6
+ *
7
+ * Usage:
8
+ * import type { Config, LintError, PluginSpec } from "nginx-lint-plugin";
9
+ */
10
+ export type { Severity, Fix, LintError, PluginSpec, } from "./generated/interfaces/nginx-lint-plugin-types.js";
11
+ export type { ArgumentType, ArgumentInfo, CommentInfo, BlankLineInfo, DirectiveData, } from "./generated/interfaces/nginx-lint-plugin-data-types.js";
12
+ export type { ConfigItem, ConfigItemDirectiveItem, ConfigItemCommentItem, ConfigItemBlankLineItem, DirectiveContext, Directive, Config, } from "./generated/interfaces/nginx-lint-plugin-config-api.js";
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * nginx-lint-plugin — shared TypeScript library for nginx-lint WASM plugins.
3
+ *
4
+ * Types are auto-generated from the WIT definition (wit/nginx-lint-plugin.wit)
5
+ * by `jco types` during the build step.
6
+ *
7
+ * Usage:
8
+ * import type { Config, LintError, PluginSpec } from "nginx-lint-plugin";
9
+ */
10
+ export {};
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Parser-based testing utilities for TypeScript nginx-lint plugins.
3
+ *
4
+ * Provides `parseConfig()` to parse real nginx configuration strings
5
+ * into WIT-compatible Config objects, and `PluginTestRunner` for
6
+ * assertion-based testing.
7
+ *
8
+ * Usage:
9
+ * import { parseConfig, PluginTestRunner } from "nginx-lint-plugin/testing";
10
+ */
11
+ import type { Config } from "./generated/interfaces/nginx-lint-plugin-config-api.js";
12
+ import type { LintError, PluginSpec } from "./generated/interfaces/nginx-lint-plugin-types.js";
13
+ /**
14
+ * Parse an nginx configuration string into a WIT-compatible Config object.
15
+ *
16
+ * Uses the nginx-lint-parser WASM component for accurate parsing identical
17
+ * to the production Rust parser. The DFS traversal (allDirectivesWithContext)
18
+ * is computed on the Rust side.
19
+ */
20
+ export declare function parseConfig(source: string, opts?: {
21
+ includeContext?: string[];
22
+ }): Config;
23
+ type SpecFn = () => PluginSpec;
24
+ type CheckFn = (cfg: Config, path: string) => LintError[];
25
+ /**
26
+ * Test runner for TypeScript nginx-lint plugins.
27
+ *
28
+ * Mirrors the Rust `PluginTestRunner` API from `nginx-lint-plugin/testing`.
29
+ *
30
+ * Example:
31
+ * ```typescript
32
+ * import { spec, check } from "./plugin.js";
33
+ * import { PluginTestRunner } from "nginx-lint-plugin/testing";
34
+ *
35
+ * const runner = new PluginTestRunner(spec, check);
36
+ * runner.assertErrors("http { server_tokens on; }", 1);
37
+ * runner.assertErrors("http { server_tokens off; }", 0);
38
+ * ```
39
+ */
40
+ export declare class PluginTestRunner {
41
+ private specFn;
42
+ private checkFn;
43
+ constructor(spec: SpecFn, check: CheckFn);
44
+ /**
45
+ * Parse and check a config string, returning only errors from this plugin's rule.
46
+ */
47
+ checkString(content: string, opts?: {
48
+ includeContext?: string[];
49
+ }): LintError[];
50
+ /**
51
+ * Assert that parsing and checking a config produces exactly `count` errors
52
+ * from this plugin's rule.
53
+ */
54
+ assertErrors(content: string, count: number): void;
55
+ /**
56
+ * Assert that a config produces at least one error on the given line.
57
+ */
58
+ assertErrorOnLine(content: string, line: number): void;
59
+ /**
60
+ * Test bad/good config content.
61
+ * - `badConf` must produce at least one error.
62
+ * - `goodConf` must produce zero errors.
63
+ */
64
+ testExamples(badConf: string, goodConf: string): void;
65
+ }
66
+ export {};
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Parser-based testing utilities for TypeScript nginx-lint plugins.
3
+ *
4
+ * Provides `parseConfig()` to parse real nginx configuration strings
5
+ * into WIT-compatible Config objects, and `PluginTestRunner` for
6
+ * assertion-based testing.
7
+ *
8
+ * Usage:
9
+ * import { parseConfig, PluginTestRunner } from "nginx-lint-plugin/testing";
10
+ */
11
+ import { parseConfig as parseConfigWasm } from "../wasm/parser/parser.js";
12
+ import { buildConfigFromParseOutput } from "./config-builder.js";
13
+ /**
14
+ * Parse an nginx configuration string into a WIT-compatible Config object.
15
+ *
16
+ * Uses the nginx-lint-parser WASM component for accurate parsing identical
17
+ * to the production Rust parser. The DFS traversal (allDirectivesWithContext)
18
+ * is computed on the Rust side.
19
+ */
20
+ export function parseConfig(source, opts) {
21
+ const output = parseConfigWasm(source, opts?.includeContext ?? []);
22
+ return buildConfigFromParseOutput(output);
23
+ }
24
+ /**
25
+ * Test runner for TypeScript nginx-lint plugins.
26
+ *
27
+ * Mirrors the Rust `PluginTestRunner` API from `nginx-lint-plugin/testing`.
28
+ *
29
+ * Example:
30
+ * ```typescript
31
+ * import { spec, check } from "./plugin.js";
32
+ * import { PluginTestRunner } from "nginx-lint-plugin/testing";
33
+ *
34
+ * const runner = new PluginTestRunner(spec, check);
35
+ * runner.assertErrors("http { server_tokens on; }", 1);
36
+ * runner.assertErrors("http { server_tokens off; }", 0);
37
+ * ```
38
+ */
39
+ export class PluginTestRunner {
40
+ specFn;
41
+ checkFn;
42
+ constructor(spec, check) {
43
+ this.specFn = spec;
44
+ this.checkFn = check;
45
+ }
46
+ /**
47
+ * Parse and check a config string, returning only errors from this plugin's rule.
48
+ */
49
+ checkString(content, opts) {
50
+ const cfg = parseConfig(content, opts);
51
+ const errors = this.checkFn(cfg, "test.conf");
52
+ const ruleName = this.specFn().name;
53
+ return errors.filter((e) => e.rule === ruleName);
54
+ }
55
+ /**
56
+ * Assert that parsing and checking a config produces exactly `count` errors
57
+ * from this plugin's rule.
58
+ */
59
+ assertErrors(content, count) {
60
+ const errors = this.checkString(content);
61
+ if (errors.length !== count) {
62
+ throw new Error(`Expected ${count} error(s) from "${this.specFn().name}", got ${errors.length}: ${JSON.stringify(errors, null, 2)}`);
63
+ }
64
+ }
65
+ /**
66
+ * Assert that a config produces at least one error on the given line.
67
+ */
68
+ assertErrorOnLine(content, line) {
69
+ const errors = this.checkString(content);
70
+ const hasLine = errors.some((e) => e.line === line);
71
+ if (!hasLine) {
72
+ const lines = errors.map((e) => e.line);
73
+ throw new Error(`Expected error on line ${line} from "${this.specFn().name}", got errors on lines: ${JSON.stringify(lines)}`);
74
+ }
75
+ }
76
+ /**
77
+ * Test bad/good config content.
78
+ * - `badConf` must produce at least one error.
79
+ * - `goodConf` must produce zero errors.
80
+ */
81
+ testExamples(badConf, goodConf) {
82
+ const ruleName = this.specFn().name;
83
+ const badErrors = this.checkString(badConf);
84
+ if (badErrors.length === 0) {
85
+ throw new Error(`bad.conf should produce at least one "${ruleName}" error, got none`);
86
+ }
87
+ const goodErrors = this.checkString(goodConf);
88
+ if (goodErrors.length > 0) {
89
+ throw new Error(`good.conf should produce no "${ruleName}" errors, got: ${JSON.stringify(goodErrors, null, 2)}`);
90
+ }
91
+ }
92
+ }
package/package.json CHANGED
@@ -1,10 +1,31 @@
1
1
  {
2
2
  "name": "nginx-lint-plugin",
3
- "version": "0.0.1",
4
- "description": "OIDC trusted publishing setup package for nginx-lint-plugin",
5
- "keywords": [
6
- "oidc",
7
- "trusted-publishing",
8
- "setup"
9
- ]
3
+ "version": "0.8.2",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/walf443/nginx-lint",
9
+ "directory": "plugins/typescript/nginx-lint-plugin"
10
+ },
11
+ "main": "dist/index.js",
12
+ "types": "dist/index.d.ts",
13
+ "exports": {
14
+ ".": "./dist/index.js",
15
+ "./testing": "./dist/plugin-test-runner.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "wit"
20
+ ],
21
+ "scripts": {
22
+ "copy-wit": "mkdir -p wit && cp ../../../wit/nginx-lint-plugin.wit wit/",
23
+ "generate": "jco types wit -n plugin -o src/generated",
24
+ "build": "npm run copy-wit && npm run generate && tsc",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "devDependencies": {
28
+ "@bytecodealliance/jco": "^1",
29
+ "typescript": "^5"
30
+ }
10
31
  }
@@ -0,0 +1,281 @@
1
+ package nginx-lint:plugin@3.0.0;
2
+
3
+ interface types {
4
+ enum severity {
5
+ error,
6
+ warning,
7
+ }
8
+
9
+ record fix {
10
+ line: u32,
11
+ old-text: option<string>,
12
+ new-text: string,
13
+ delete-line: bool,
14
+ insert-after: bool,
15
+ start-offset: option<u32>,
16
+ end-offset: option<u32>,
17
+ }
18
+
19
+ record lint-error {
20
+ rule: string,
21
+ category: string,
22
+ message: string,
23
+ severity: severity,
24
+ line: option<u32>,
25
+ column: option<u32>,
26
+ fixes: list<fix>,
27
+ }
28
+
29
+ record plugin-spec {
30
+ name: string,
31
+ category: string,
32
+ description: string,
33
+ api-version: string,
34
+ severity: option<string>,
35
+ why: option<string>,
36
+ bad-example: option<string>,
37
+ good-example: option<string>,
38
+ references: option<list<string>>,
39
+ }
40
+ }
41
+
42
+ /// Shared data types used by both the plugin config-api (resource-based)
43
+ /// and the parser output (record-based).
44
+ interface data-types {
45
+ /// Argument value type
46
+ enum argument-type {
47
+ literal,
48
+ quoted-string,
49
+ single-quoted-string,
50
+ variable,
51
+ }
52
+
53
+ /// Argument data (returned as a record since it's small)
54
+ record argument-info {
55
+ value: string,
56
+ raw: string,
57
+ arg-type: argument-type,
58
+ line: u32,
59
+ column: u32,
60
+ start-offset: u32,
61
+ end-offset: u32,
62
+ }
63
+
64
+ /// Comment data
65
+ record comment-info {
66
+ text: string,
67
+ line: u32,
68
+ column: u32,
69
+ leading-whitespace: string,
70
+ trailing-whitespace: string,
71
+ start-offset: u32,
72
+ end-offset: u32,
73
+ }
74
+
75
+ /// Blank line data
76
+ record blank-line-info {
77
+ line: u32,
78
+ content: string,
79
+ start-offset: u32,
80
+ }
81
+
82
+ /// All flat properties of a directive (for bulk retrieval)
83
+ record directive-data {
84
+ name: string,
85
+ args: list<argument-info>,
86
+ line: u32,
87
+ column: u32,
88
+ start-offset: u32,
89
+ end-offset: u32,
90
+ end-line: u32,
91
+ end-column: u32,
92
+ leading-whitespace: string,
93
+ trailing-whitespace: string,
94
+ space-before-terminator: string,
95
+ has-block: bool,
96
+ block-is-raw: bool,
97
+ /// Actual raw content for raw blocks (e.g., lua code); none if not a raw block
98
+ block-raw-content: option<string>,
99
+ /// Leading whitespace before the closing brace; none if no block
100
+ closing-brace-leading-whitespace: option<string>,
101
+ /// Trailing whitespace after the closing brace; none if no block
102
+ block-trailing-whitespace: option<string>,
103
+ /// Text of the trailing comment on the directive line; none if no comment
104
+ trailing-comment-text: option<string>,
105
+ /// End column of the name token (1-based)
106
+ name-end-column: u32,
107
+ /// End byte offset of the name token (0-based)
108
+ name-end-offset: u32,
109
+ /// Start line of the block's opening brace (1-based); none if no block
110
+ block-start-line: option<u32>,
111
+ /// Start column of the block's opening brace (1-based); none if no block
112
+ block-start-column: option<u32>,
113
+ /// Start byte offset of the block's opening brace (0-based); none if no block
114
+ block-start-offset: option<u32>,
115
+ }
116
+ }
117
+
118
+ interface config-api {
119
+ use types.{fix};
120
+ use data-types.{argument-type, argument-info, comment-info, blank-line-info, directive-data};
121
+
122
+ /// A config item (directive, comment, or blank line)
123
+ variant config-item {
124
+ directive-item(directive),
125
+ comment-item(comment-info),
126
+ blank-line-item(blank-line-info),
127
+ }
128
+
129
+ /// A directive paired with its parent block context
130
+ record directive-context {
131
+ directive: directive,
132
+ parent-stack: list<string>,
133
+ depth: u32,
134
+ }
135
+
136
+ /// An nginx directive (resource backed by host data)
137
+ resource directive {
138
+ /// Get all flat properties in a single call (optimized for reconstruction)
139
+ data: func() -> directive-data;
140
+ /// Get the directive name
141
+ name: func() -> string;
142
+ /// Check if the directive has the given name
143
+ is: func(name: string) -> bool;
144
+
145
+ /// Get the first argument value
146
+ first-arg: func() -> option<string>;
147
+ /// Check if the first argument equals the given value
148
+ first-arg-is: func(value: string) -> bool;
149
+ /// Get the argument at the given index
150
+ arg-at: func(index: u32) -> option<string>;
151
+ /// Get the last argument value
152
+ last-arg: func() -> option<string>;
153
+ /// Check if any argument equals the given value
154
+ has-arg: func(value: string) -> bool;
155
+ /// Return the number of arguments
156
+ arg-count: func() -> u32;
157
+ /// Get all arguments as records
158
+ args: func() -> list<argument-info>;
159
+
160
+ /// Get the start line number (1-based)
161
+ line: func() -> u32;
162
+ /// Get the start column number (1-based)
163
+ column: func() -> u32;
164
+ /// Get the start byte offset (0-based)
165
+ start-offset: func() -> u32;
166
+ /// Get the end byte offset (0-based)
167
+ end-offset: func() -> u32;
168
+ /// Get the leading whitespace before the directive
169
+ leading-whitespace: func() -> string;
170
+ /// Get the trailing whitespace after the directive
171
+ trailing-whitespace: func() -> string;
172
+ /// Get the space before the terminator (; or {)
173
+ space-before-terminator: func() -> string;
174
+
175
+ /// Check if the directive has a block
176
+ has-block: func() -> bool;
177
+ /// Get the items inside the directive's block
178
+ block-items: func() -> list<config-item>;
179
+ /// Check if the block contains raw content (e.g., lua blocks)
180
+ block-is-raw: func() -> bool;
181
+
182
+ /// Create a fix that replaces this directive with new text
183
+ replace-with: func(new-text: string) -> fix;
184
+ /// Create a fix that deletes this directive's line
185
+ delete-line-fix: func() -> fix;
186
+ /// Create a fix that inserts a new line after this directive
187
+ insert-after: func(new-text: string) -> fix;
188
+ /// Create a fix that inserts a new line before this directive
189
+ insert-before: func(new-text: string) -> fix;
190
+ /// Create a fix that inserts multiple lines after this directive
191
+ insert-after-many: func(lines: list<string>) -> fix;
192
+ /// Create a fix that inserts multiple lines before this directive
193
+ insert-before-many: func(lines: list<string>) -> fix;
194
+ }
195
+
196
+ /// The parsed nginx configuration (resource backed by host data)
197
+ resource config {
198
+ /// Iterate over all directives recursively with parent context
199
+ all-directives-with-context: func() -> list<directive-context>;
200
+ /// Iterate over all directives recursively
201
+ all-directives: func() -> list<directive>;
202
+ /// Get the top-level config items
203
+ items: func() -> list<config-item>;
204
+
205
+ /// Get the include context (parent block names from include directive)
206
+ include-context: func() -> list<string>;
207
+ /// Check if this config is included from within a specific context
208
+ is-included-from: func(context: string) -> bool;
209
+ /// Check if included from http context
210
+ is-included-from-http: func() -> bool;
211
+ /// Check if included from http > server context
212
+ is-included-from-http-server: func() -> bool;
213
+ /// Check if included from http > ... > location context
214
+ is-included-from-http-location: func() -> bool;
215
+ /// Check if included from stream context
216
+ is-included-from-stream: func() -> bool;
217
+ /// Get the immediate parent context
218
+ immediate-parent-context: func() -> option<string>;
219
+ }
220
+ }
221
+
222
+ /// Record-based types for parser output (no resources, no recursion).
223
+ /// Uses index-based references to represent the tree structure:
224
+ /// all config items are stored in a flat array, with child-indices
225
+ /// pointing to children within that array.
226
+ interface parser-types {
227
+ use data-types.{comment-info, blank-line-info, directive-data};
228
+
229
+ /// The kind of a config item (non-recursive)
230
+ variant config-item-value {
231
+ directive-item(directive-data),
232
+ comment-item(comment-info),
233
+ blank-line-item(blank-line-info),
234
+ }
235
+
236
+ /// A config item in the flat all-items array.
237
+ /// For directive items with blocks, child-indices contains the indices
238
+ /// of child items within the all-items array.
239
+ record config-item {
240
+ value: config-item-value,
241
+ child-indices: list<u32>,
242
+ }
243
+
244
+ /// A directive paired with its parent block context
245
+ record directive-context {
246
+ data: directive-data,
247
+ /// Indices of block items in the all-items array
248
+ block-item-indices: list<u32>,
249
+ parent-stack: list<string>,
250
+ depth: u32,
251
+ }
252
+
253
+ /// Complete parser output
254
+ record parse-output {
255
+ directives-with-context: list<directive-context>,
256
+ include-context: list<string>,
257
+ /// Flat array of all config items (DFS order)
258
+ all-items: list<config-item>,
259
+ /// Indices of top-level items in all-items
260
+ top-level-indices: list<u32>,
261
+ }
262
+ }
263
+
264
+ world plugin {
265
+ use types.{plugin-spec, lint-error};
266
+ use config-api.{config};
267
+ import config-api;
268
+
269
+ /// Return plugin metadata
270
+ export spec: func() -> plugin-spec;
271
+
272
+ /// Check config and return lint errors
273
+ export check: func(cfg: borrow<config>, path: string) -> list<lint-error>;
274
+ }
275
+
276
+ world parser {
277
+ use parser-types.{parse-output};
278
+
279
+ /// Parse nginx config source and return structured output
280
+ export parse-config: func(source: string, include-context: list<string>) -> result<parse-output, string>;
281
+ }