norn-cli 1.6.0 → 1.6.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.
Files changed (40) hide show
  1. package/AGENTS.md +9 -1
  2. package/CHANGELOG.md +23 -0
  3. package/dist/cli.js +246 -80
  4. package/package.json +1 -1
  5. package/out/assertionRunner.js +0 -537
  6. package/out/chatParticipant.js +0 -722
  7. package/out/cli/colors.js +0 -129
  8. package/out/cli/formatters/assertion.js +0 -75
  9. package/out/cli/formatters/index.js +0 -23
  10. package/out/cli/formatters/response.js +0 -106
  11. package/out/cli/formatters/summary.js +0 -187
  12. package/out/cli/redaction.js +0 -237
  13. package/out/cli/reporters/html.js +0 -634
  14. package/out/cli/reporters/index.js +0 -22
  15. package/out/cli/reporters/junit.js +0 -211
  16. package/out/cli.js +0 -989
  17. package/out/codeLensProvider.js +0 -248
  18. package/out/compareContentProvider.js +0 -85
  19. package/out/completionProvider.js +0 -2404
  20. package/out/contractDecorationProvider.js +0 -243
  21. package/out/coverageCalculator.js +0 -837
  22. package/out/coveragePanel.js +0 -545
  23. package/out/diagnosticProvider.js +0 -1113
  24. package/out/environmentProvider.js +0 -442
  25. package/out/extension.js +0 -1114
  26. package/out/httpClient.js +0 -269
  27. package/out/jsonFileReader.js +0 -320
  28. package/out/nornPrompt.js +0 -580
  29. package/out/nornapiParser.js +0 -326
  30. package/out/parser.js +0 -725
  31. package/out/responsePanel.js +0 -4674
  32. package/out/schemaGenerator.js +0 -393
  33. package/out/scriptRunner.js +0 -419
  34. package/out/sequenceRunner.js +0 -3046
  35. package/out/swaggerBodyIntellisenseCache.js +0 -147
  36. package/out/swaggerParser.js +0 -419
  37. package/out/test/coverageCalculator.test.js +0 -100
  38. package/out/test/extension.test.js +0 -48
  39. package/out/testProvider.js +0 -658
  40. package/out/validationCache.js +0 -245
package/out/extension.js DELETED
@@ -1,1114 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.activate = activate;
37
- exports.saveSchemaValidationResults = saveSchemaValidationResults;
38
- exports.deactivate = deactivate;
39
- const vscode = __importStar(require("vscode"));
40
- const path = __importStar(require("path"));
41
- const fs = __importStar(require("fs/promises"));
42
- const fsSync = __importStar(require("fs"));
43
- const parser_1 = require("./parser");
44
- const httpClient_1 = require("./httpClient");
45
- const codeLensProvider_1 = require("./codeLensProvider");
46
- const completionProvider_1 = require("./completionProvider");
47
- const responsePanel_1 = require("./responsePanel");
48
- const diagnosticProvider_1 = require("./diagnosticProvider");
49
- const sequenceRunner_1 = require("./sequenceRunner");
50
- const environmentProvider_1 = require("./environmentProvider");
51
- const swaggerParser_1 = require("./swaggerParser");
52
- const nornapiParser_1 = require("./nornapiParser");
53
- const testProvider_1 = require("./testProvider");
54
- const coverageCalculator_1 = require("./coverageCalculator");
55
- const coveragePanel_1 = require("./coveragePanel");
56
- const compareContentProvider_1 = require("./compareContentProvider");
57
- const contractDecorationProvider_1 = require("./contractDecorationProvider");
58
- const validationCache_1 = require("./validationCache");
59
- const schemaGenerator_1 = require("./schemaGenerator");
60
- const swaggerBodyIntellisenseCache_1 = require("./swaggerBodyIntellisenseCache");
61
- const chatParticipant_1 = require("./chatParticipant");
62
- // Module-level reference to contract decoration provider for refreshing after sequence runs
63
- let contractDecorationProviderInstance;
64
- function activate(context) {
65
- console.log('Norn extension activated');
66
- // Register content provider for response comparison diff
67
- (0, compareContentProvider_1.registerCompareContentProvider)(context);
68
- // Register Test Explorer integration
69
- const testController = new testProvider_1.NornTestController();
70
- context.subscriptions.push(testController);
71
- // Register @norn Copilot Chat participant (gracefully skipped if Copilot is not installed)
72
- (0, chatParticipant_1.registerChatParticipant)(context);
73
- const sendRequestCommand = vscode.commands.registerCommand('norn.sendRequest', (lineFromCodeLens) => processEditorsInput(context.extensionUri, lineFromCodeLens));
74
- const runSequenceCommand = vscode.commands.registerCommand('norn.runSequence', (lineFromCodeLens) => processSequence(context.extensionUri, lineFromCodeLens));
75
- const clearCookiesCommand = vscode.commands.registerCommand('norn.clearCookies', async () => {
76
- (0, httpClient_1.clearCookies)();
77
- vscode.window.showInformationMessage('Norn: All cookies cleared');
78
- });
79
- const showCookiesCommand = vscode.commands.registerCommand('norn.showCookies', async () => {
80
- const cookies = await (0, httpClient_1.getAllCookies)();
81
- if (cookies.length === 0) {
82
- vscode.window.showInformationMessage('Norn: No cookies stored');
83
- }
84
- else {
85
- const items = cookies.map(c => `${c.domain}: ${c.name}=${c.value.substring(0, 30)}${c.value.length > 30 ? '...' : ''}`);
86
- vscode.window.showQuickPick(items, { title: 'Stored Cookies', canPickMany: false });
87
- }
88
- });
89
- // Register environment selector command
90
- const selectEnvironmentCommand = vscode.commands.registerCommand('norn.selectEnvironment', async () => {
91
- await (0, environmentProvider_1.showEnvironmentPicker)();
92
- vscode.commands.executeCommand('norn.refreshDiagnostics');
93
- });
94
- // Register environment selector with refresh (for CodeLens)
95
- const selectEnvironmentAndRefreshCommand = vscode.commands.registerCommand('norn.selectEnvironmentAndRefresh', async () => {
96
- await (0, environmentProvider_1.showEnvironmentPicker)();
97
- vscode.commands.executeCommand('norn.refreshCodeLenses');
98
- vscode.commands.executeCommand('norn.refreshDiagnostics');
99
- });
100
- // Register command to create .nornenv file
101
- const createEnvFileCommand = vscode.commands.registerCommand('norn.createEnvFile', async () => {
102
- const workspaceFolders = vscode.workspace.workspaceFolders;
103
- if (!workspaceFolders) {
104
- vscode.window.showErrorMessage('No workspace folder open');
105
- return;
106
- }
107
- // If multiple folders, ask which one
108
- let targetFolder = workspaceFolders[0];
109
- if (workspaceFolders.length > 1) {
110
- const picked = await vscode.window.showWorkspaceFolderPick({
111
- placeHolder: 'Select workspace folder for .nornenv file'
112
- });
113
- if (!picked) {
114
- return;
115
- }
116
- targetFolder = picked;
117
- }
118
- const envFilePath = vscode.Uri.joinPath(targetFolder.uri, '.nornenv');
119
- const template = `# Norn Environment Configuration
120
- # Variables defined here are available in your .norn files as {{variableName}}
121
-
122
- # Common variables (available in all environments)
123
- var baseUrl = https://api.example.com
124
-
125
- # Development environment
126
- [env:dev]
127
- var apiKey = dev-api-key-here
128
- var debug = true
129
-
130
- # Production environment
131
- [env:prod]
132
- var apiKey = prod-api-key-here
133
- var debug = false
134
- `;
135
- try {
136
- await vscode.workspace.fs.writeFile(envFilePath, Buffer.from(template, 'utf8'));
137
- const doc = await vscode.workspace.openTextDocument(envFilePath);
138
- await vscode.window.showTextDocument(doc);
139
- vscode.commands.executeCommand('norn.refreshCodeLenses');
140
- vscode.window.showInformationMessage('Created .nornenv file. Select an environment from the status bar.');
141
- }
142
- catch (error) {
143
- vscode.window.showErrorMessage(`Failed to create .nornenv: ${error}`);
144
- }
145
- });
146
- // Register command to import endpoints from Swagger/OpenAPI spec
147
- const importSwaggerCommand = vscode.commands.registerCommand('norn.importSwagger', async (swaggerUrl, lineNumber) => {
148
- const editor = vscode.window.activeTextEditor;
149
- if (!editor) {
150
- vscode.window.showErrorMessage('No active editor');
151
- return;
152
- }
153
- try {
154
- // Show progress while fetching
155
- await vscode.window.withProgress({
156
- location: vscode.ProgressLocation.Notification,
157
- title: 'Parsing OpenAPI spec...',
158
- cancellable: false
159
- }, async (progress) => {
160
- progress.report({ message: 'Fetching specification...' });
161
- // Parse the swagger spec
162
- const spec = await (0, swaggerParser_1.parseSwaggerSpec)(swaggerUrl);
163
- progress.report({ message: 'Spec loaded!' });
164
- // Build QuickPick items for sections
165
- const sectionItems = [
166
- { label: 'All', description: `Import all ${spec.sections.reduce((acc, s) => acc + s.endpoints.length, 0)} endpoints`, picked: true }
167
- ];
168
- for (const section of spec.sections) {
169
- sectionItems.push({
170
- label: section.name,
171
- description: section.description || `${section.endpoints.length} endpoints`
172
- });
173
- }
174
- // Show section picker
175
- const selectedSections = await vscode.window.showQuickPick(sectionItems, {
176
- canPickMany: true,
177
- placeHolder: `Select sections to import from ${spec.title}`,
178
- title: 'Import OpenAPI Endpoints'
179
- });
180
- if (!selectedSections || selectedSections.length === 0) {
181
- return;
182
- }
183
- const sectionNames = selectedSections.map(s => s.label);
184
- // Ask for custom baseUrl
185
- const baseUrlInput = await vscode.window.showInputBox({
186
- prompt: 'Enter custom base URL (or leave empty to use spec default)',
187
- placeHolder: spec.baseUrl || 'https://api.example.com',
188
- value: spec.baseUrl
189
- });
190
- // User cancelled
191
- if (baseUrlInput === undefined) {
192
- return;
193
- }
194
- // Cache request body schemas for IntelliSense (best effort)
195
- try {
196
- progress.report({ message: 'Caching request body schemas...' });
197
- const requestSchemas = await (0, swaggerParser_1.extractRequestBodySchemas)(swaggerUrl);
198
- (0, swaggerBodyIntellisenseCache_1.saveRequestBodySchemasForUrl)(swaggerUrl, baseUrlInput || spec.baseUrl, requestSchemas);
199
- }
200
- catch {
201
- // Keep endpoint import successful even if schema caching fails
202
- }
203
- // Generate the content
204
- const content = (0, swaggerParser_1.generateNornapiContent)(spec, sectionNames, baseUrlInput || undefined);
205
- // Find position to insert - after the swagger line
206
- const document = editor.document;
207
- const swaggerLine = document.lineAt(lineNumber);
208
- const insertPosition = new vscode.Position(lineNumber + 1, 0);
209
- // Insert the generated content
210
- await editor.edit(editBuilder => {
211
- editBuilder.insert(insertPosition, '\n' + content + '\n');
212
- });
213
- vscode.window.showInformationMessage(`Imported ${sectionNames.includes('All') ? 'all sections' : sectionNames.join(', ')} from ${spec.title}`);
214
- });
215
- }
216
- catch (error) {
217
- const errorMessage = error instanceof Error ? error.message : String(error);
218
- vscode.window.showErrorMessage(`Failed to parse OpenAPI spec: ${errorMessage}`);
219
- }
220
- });
221
- // Register command to generate JSON Schemas from Swagger/OpenAPI spec
222
- const generateSchemasFromSwaggerCommand = vscode.commands.registerCommand('norn.generateSchemasFromSwagger', async (swaggerUrl, sourceFile) => {
223
- try {
224
- const schemas = await vscode.window.withProgress({
225
- location: vscode.ProgressLocation.Notification,
226
- title: 'Extracting schemas from OpenAPI spec...',
227
- cancellable: false
228
- }, async (progress) => {
229
- progress.report({ message: 'Fetching specification...' });
230
- return await (0, swaggerParser_1.extractResponseSchemas)(swaggerUrl);
231
- });
232
- if (schemas.length === 0) {
233
- vscode.window.showWarningMessage('No response schemas found in the OpenAPI spec');
234
- return;
235
- }
236
- // Ask user for output folder
237
- const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
238
- const defaultFolder = workspaceRoot ? path.join(workspaceRoot, 'contracts') : path.dirname(sourceFile);
239
- const folderItems = [
240
- { label: 'contracts/', description: `${defaultFolder}`, picked: true },
241
- { label: 'schemas/', description: path.join(path.dirname(sourceFile), 'schemas') },
242
- { label: 'Choose folder...', description: 'Select a custom folder' }
243
- ];
244
- const selectedFolder = await vscode.window.showQuickPick(folderItems, {
245
- placeHolder: 'Select output folder for schema files',
246
- title: `Generate ${schemas.length} Schemas`
247
- });
248
- if (!selectedFolder) {
249
- return;
250
- }
251
- let outputFolder;
252
- if (selectedFolder.label === 'Choose folder...') {
253
- const folderUri = await vscode.window.showOpenDialog({
254
- canSelectFiles: false,
255
- canSelectFolders: true,
256
- canSelectMany: false,
257
- title: 'Select output folder for schemas'
258
- });
259
- if (!folderUri || folderUri.length === 0) {
260
- return;
261
- }
262
- outputFolder = folderUri[0].fsPath;
263
- }
264
- else if (selectedFolder.label === 'contracts/') {
265
- outputFolder = defaultFolder;
266
- }
267
- else {
268
- outputFolder = path.join(path.dirname(sourceFile), 'schemas');
269
- }
270
- // Create folder if it doesn't exist
271
- if (!fsSync.existsSync(outputFolder)) {
272
- fsSync.mkdirSync(outputFolder, { recursive: true });
273
- }
274
- // Filter schemas to generate (allow user to select)
275
- const schemaItems = schemas.map((s) => ({
276
- label: (0, swaggerParser_1.generateSchemaFilename)(s.operationId, s.statusCode),
277
- description: `${s.method} ${s.path} → ${s.statusCode}`,
278
- picked: s.statusCode === '200' // Default select 200 responses
279
- }));
280
- const selectedSchemas = await vscode.window.showQuickPick(schemaItems, {
281
- canPickMany: true,
282
- placeHolder: 'Select schemas to generate',
283
- title: `${schemas.length} response schemas available`
284
- });
285
- if (!selectedSchemas || selectedSchemas.length === 0) {
286
- return;
287
- }
288
- const selectedFilenames = new Set(selectedSchemas.map(s => s.label));
289
- // Write schema files
290
- let createdCount = 0;
291
- for (const schema of schemas) {
292
- const filename = (0, swaggerParser_1.generateSchemaFilename)(schema.operationId, schema.statusCode);
293
- if (!selectedFilenames.has(filename)) {
294
- continue;
295
- }
296
- const filePath = path.join(outputFolder, filename);
297
- const content = JSON.stringify(schema.schema, null, 2);
298
- fsSync.writeFileSync(filePath, content, 'utf-8');
299
- createdCount++;
300
- }
301
- vscode.window.showInformationMessage(`Generated ${createdCount} schema files in ${path.basename(outputFolder)}/`);
302
- }
303
- catch (error) {
304
- const errorMessage = error instanceof Error ? error.message : String(error);
305
- vscode.window.showErrorMessage(`Failed to generate schemas: ${errorMessage}`);
306
- }
307
- });
308
- // === Coverage Feature Setup ===
309
- // Create coverage status bar item
310
- const coverageStatusBarItem = (0, environmentProvider_1.createCoverageStatusBarItem)();
311
- // Register command to show coverage panel
312
- const showCoverageCommand = vscode.commands.registerCommand('norn.showCoverage', async (nornapiFilePath) => {
313
- try {
314
- const coverage = nornapiFilePath
315
- ? await (0, coverageCalculator_1.getCoverageForNornapiFile)(nornapiFilePath)
316
- : await (0, coverageCalculator_1.getCoverage)();
317
- coveragePanel_1.CoveragePanel.show(coverage, nornapiFilePath);
318
- }
319
- catch (error) {
320
- const errorMessage = error instanceof Error ? error.message : String(error);
321
- vscode.window.showErrorMessage(`Failed to calculate coverage: ${errorMessage}`);
322
- }
323
- });
324
- // Register command to refresh coverage
325
- const refreshCoverageCommand = vscode.commands.registerCommand('norn.refreshCoverage', async (nornapiFilePath) => {
326
- try {
327
- (0, swaggerParser_1.invalidateSwaggerCache)();
328
- const coverage = nornapiFilePath
329
- ? await (0, coverageCalculator_1.refreshCoverageForNornapiFile)(nornapiFilePath)
330
- : await (0, coverageCalculator_1.refreshCoverage)();
331
- coveragePanel_1.CoveragePanel.updateContent(coverage);
332
- vscode.window.showInformationMessage(`Coverage refreshed: ${coverage.percentage}%`);
333
- }
334
- catch (error) {
335
- const errorMessage = error instanceof Error ? error.message : String(error);
336
- vscode.window.showErrorMessage(`Failed to refresh coverage: ${errorMessage}`);
337
- }
338
- });
339
- const refreshSwaggerBodySchemasFromUrls = async (urls) => {
340
- for (const swaggerUrl of urls) {
341
- try {
342
- const spec = await (0, swaggerParser_1.parseSwaggerSpec)(swaggerUrl);
343
- const requestSchemas = await (0, swaggerParser_1.extractRequestBodySchemas)(swaggerUrl);
344
- (0, swaggerBodyIntellisenseCache_1.saveRequestBodySchemasForUrl)(swaggerUrl, spec.baseUrl, requestSchemas);
345
- }
346
- catch {
347
- // Best effort cache refresh for IntelliSense
348
- }
349
- }
350
- };
351
- const warmSwaggerBodySchemaCacheFromWorkspace = async () => {
352
- try {
353
- const nornapiFiles = await vscode.workspace.findFiles('**/*.nornapi', '**/node_modules/**');
354
- const urls = new Set();
355
- for (const file of nornapiFiles) {
356
- try {
357
- const content = fsSync.readFileSync(file.fsPath, 'utf-8');
358
- for (const url of extractSwaggerUrlsFromNornapi(content)) {
359
- urls.add(url);
360
- }
361
- }
362
- catch {
363
- // Ignore individual file read errors
364
- }
365
- }
366
- await refreshSwaggerBodySchemasFromUrls(Array.from(urls));
367
- }
368
- catch {
369
- // Ignore workspace scan failures
370
- }
371
- };
372
- // Listen for coverage updates to update status bar and CodeLens
373
- const coverageUpdateListener = (0, coverageCalculator_1.onCoverageUpdate)((coverage) => {
374
- (0, environmentProvider_1.updateCoverageStatusBar)(coverage.hasSwagger, coverage.percentage, coverage.total, coverage.covered);
375
- (0, codeLensProvider_1.updateCodeLensCoverage)(coverage.percentage, coverage.total, coverage.covered);
376
- vscode.commands.executeCommand('norn.refreshCodeLenses');
377
- });
378
- // File watcher for .nornapi files - only recalculate when files are created/deleted
379
- // (might add new swagger URLs that need fetching)
380
- const nornapiFileWatcher = vscode.workspace.createFileSystemWatcher('**/*.nornapi');
381
- const scheduleNornapiRefresh = debounce(async () => {
382
- (0, coverageCalculator_1.clearCoverageCache)();
383
- // Recalculate - this will fetch new swagger URLs if detected
384
- try {
385
- await (0, coverageCalculator_1.getCoverage)();
386
- }
387
- catch {
388
- // Ignore errors in background refresh
389
- }
390
- }, 1000);
391
- // Only watch for new/deleted .nornapi files (new swagger URLs)
392
- nornapiFileWatcher.onDidCreate(() => scheduleNornapiRefresh());
393
- nornapiFileWatcher.onDidDelete(() => scheduleNornapiRefresh());
394
- // Watch for .norn and .nornapi file saves to update coverage
395
- const scheduleCoverageUpdate = debounce(async () => {
396
- (0, coverageCalculator_1.clearCoverageCache)();
397
- try {
398
- await (0, coverageCalculator_1.getCoverage)();
399
- }
400
- catch {
401
- // Ignore errors in background refresh
402
- }
403
- }, 500);
404
- const documentSaveWatcher = vscode.workspace.onDidSaveTextDocument((document) => {
405
- if (document.languageId === 'norn' || document.languageId === 'nornapi') {
406
- scheduleCoverageUpdate();
407
- }
408
- if (document.languageId === 'nornapi') {
409
- const swaggerUrls = extractSwaggerUrlsFromNornapi(document.getText());
410
- if (swaggerUrls.length > 0) {
411
- void refreshSwaggerBodySchemasFromUrls(swaggerUrls);
412
- }
413
- }
414
- });
415
- // Initial coverage calculation (delayed to not slow down activation)
416
- setTimeout(async () => {
417
- try {
418
- await (0, coverageCalculator_1.getCoverage)();
419
- }
420
- catch {
421
- // Ignore errors in initial coverage calculation
422
- }
423
- }, 2000);
424
- // Warm Swagger request-body cache in background for IntelliSense.
425
- setTimeout(() => {
426
- void warmSwaggerBodySchemaCacheFromWorkspace();
427
- }, 2500);
428
- // Create status bar item for environment
429
- const statusBarItem = (0, environmentProvider_1.createStatusBarItem)();
430
- // Register the CodeLens provider for .norn and .nornapi files
431
- const codeLensProvider = vscode.languages.registerCodeLensProvider([
432
- { language: 'norn', scheme: 'file' },
433
- { language: 'nornapi', scheme: 'file' }
434
- ], new codeLensProvider_1.HttpCodeLensProvider());
435
- // Register the Completion provider for IntelliSense
436
- const completionProvider = vscode.languages.registerCompletionItemProvider([
437
- { language: 'norn', scheme: 'file' },
438
- { language: 'nornapi', scheme: 'file' }
439
- ], new completionProvider_1.HttpCompletionProvider(),
440
- // Trigger characters:
441
- // - '{' for variable references {{
442
- // - '.' for property access ($N.body, user.body)
443
- // - ' ' for after keywords (print, run, etc.)
444
- // - '(' and ',' for parameter suggestions
445
- // - '@' for sequence tags
446
- // - ':' for header values (Content-Type: ...)
447
- // - Letters for typing variable names inside () or after print
448
- '{', '.', ' ', '(', ',', '@', ':', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '_');
449
- // Register the Diagnostic provider for undefined variables
450
- const diagnosticProvider = new diagnosticProvider_1.DiagnosticProvider();
451
- // Register command to refresh diagnostics (called when environment changes)
452
- const refreshDiagnosticsCommand = vscode.commands.registerCommand('norn.refreshDiagnostics', () => {
453
- vscode.workspace.textDocuments.forEach((document) => {
454
- if (document.languageId === 'norn' || document.languageId === 'nornenv' || document.languageId === 'nornapi') {
455
- diagnosticProvider.updateDiagnostics(document);
456
- }
457
- });
458
- });
459
- // Register command to open Contract View for schema validation errors
460
- const openContractViewCommand = vscode.commands.registerCommand('norn.openContractView', (data) => {
461
- responsePanel_1.ResponsePanel.showContractView(context.extensionUri, data);
462
- });
463
- // Register command to open a schema file from CodeLens
464
- const openSchemaFileCommand = vscode.commands.registerCommand('norn.openSchemaFile', async (schemaPath, sourceFile) => {
465
- try {
466
- // Get workspace root for @/ alias resolution
467
- const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
468
- const sourceDir = path.dirname(sourceFile);
469
- // Resolve the schema path (handles @/ alias for contracts folder)
470
- const resolvedPath = (0, schemaGenerator_1.resolveSchemaPath)(schemaPath, sourceDir, workspaceRoot);
471
- // Open the schema file
472
- const uri = vscode.Uri.file(resolvedPath);
473
- await vscode.commands.executeCommand('vscode.open', uri);
474
- }
475
- catch (error) {
476
- vscode.window.showErrorMessage(`Could not open schema file: ${schemaPath}`);
477
- }
478
- });
479
- // Register command to validate a schema assertion immediately
480
- const validateSchemaAssertionNowCommand = vscode.commands.registerCommand('norn.validateSchemaAssertionNow', async (line, sourceFile) => {
481
- try {
482
- // Find the enclosing sequence and run it
483
- const editor = vscode.window.activeTextEditor;
484
- if (!editor || editor.document.uri.fsPath !== sourceFile) {
485
- // Open the file if not already open
486
- const doc = await vscode.workspace.openTextDocument(sourceFile);
487
- await vscode.window.showTextDocument(doc);
488
- }
489
- // Run the sequence at this line
490
- await vscode.commands.executeCommand('norn.runSequence', line);
491
- }
492
- catch (error) {
493
- vscode.window.showErrorMessage(`Validation failed: ${error}`);
494
- }
495
- });
496
- // Register command to view contract validation report
497
- const viewContractReportCommand = vscode.commands.registerCommand('norn.viewContractReport', async (line, sourceFile, schemaPath) => {
498
- try {
499
- const editor = vscode.window.activeTextEditor;
500
- if (!editor || editor.document.uri.fsPath !== sourceFile) {
501
- const doc = await vscode.workspace.openTextDocument(sourceFile);
502
- await vscode.window.showTextDocument(doc);
503
- }
504
- const activeEditor = vscode.window.activeTextEditor;
505
- if (!activeEditor) {
506
- vscode.window.showErrorMessage('No active editor');
507
- return;
508
- }
509
- // Run the sequence and capture the result
510
- const fullText = activeEditor.document.getText();
511
- const workingDir = path.dirname(activeEditor.document.uri.fsPath);
512
- const sequence = (0, sequenceRunner_1.getSequenceAtLine)(fullText, line);
513
- if (!sequence) {
514
- vscode.window.showErrorMessage('No sequence found at this line');
515
- return;
516
- }
517
- // Get variables and run imports
518
- const fileLevelVariables = (0, parser_1.extractFileLevelVariables)(fullText);
519
- const envVars = (0, environmentProvider_1.getEnvironmentVariables)(activeEditor.document.uri.fsPath);
520
- const allVariables = { ...envVars, ...fileLevelVariables };
521
- const importResult = await (0, parser_1.resolveImports)(fullText, workingDir, async (filePath) => {
522
- const content = await fs.readFile(filePath, 'utf-8');
523
- return content;
524
- });
525
- const fullTextWithImports = importResult.importedContent
526
- ? `${importResult.importedContent}\n\n${fullText}`
527
- : fullText;
528
- const apiDefinitions = (importResult.headerGroups && importResult.headerGroups.length > 0) ||
529
- (importResult.endpoints && importResult.endpoints.length > 0)
530
- ? { headerGroups: importResult.headerGroups || [], endpoints: importResult.endpoints || [] }
531
- : undefined;
532
- // Run the sequence
533
- const result = await (0, sequenceRunner_1.runSequence)(sequence.content, allVariables, workingDir, fullTextWithImports, undefined, // no progress callback
534
- undefined, // callStack
535
- undefined, // defaultArgs
536
- apiDefinitions, undefined, // tagFilterOptions
537
- importResult.sequenceSources // track source files for script path resolution
538
- );
539
- // Find the matchesSchema assertion step that corresponds to this line
540
- const assertionLineRelative = line - sequence.startLine - 1;
541
- let matchingStep = result.steps.find(s => s.type === 'assertion' &&
542
- s.assertion?.operator === 'matchesSchema' &&
543
- s.lineNumber !== undefined &&
544
- Math.abs(s.lineNumber - assertionLineRelative) <= 1);
545
- // If not found by line, try to find any matchesSchema assertion with matching schema path
546
- if (!matchingStep) {
547
- matchingStep = result.steps.find(s => s.type === 'assertion' &&
548
- s.assertion?.operator === 'matchesSchema' &&
549
- (s.assertion?.schemaPath?.includes(schemaPath) || s.assertion?.rightValue?.includes(schemaPath)));
550
- }
551
- if (!matchingStep || !matchingStep.assertion) {
552
- vscode.window.showErrorMessage('Could not find the schema assertion result. Try running the full sequence first.');
553
- return;
554
- }
555
- // Get the response body used in the assertion
556
- const responseBody = matchingStep.assertion.leftValue;
557
- const schema = matchingStep.assertion.schema;
558
- const errors = matchingStep.assertion.schemaErrors || [];
559
- const resolvedSchemaPath = matchingStep.assertion.schemaPath || schemaPath;
560
- // Show the Contract Report
561
- responsePanel_1.ResponsePanel.showContractReport(context.extensionUri, {
562
- schemaPath: resolvedSchemaPath,
563
- errors,
564
- responseBody,
565
- schema,
566
- assertionLine: line,
567
- sourceFile
568
- });
569
- // Save validation results and refresh decorations
570
- saveSchemaValidationResults(result, activeEditor.document.uri.fsPath, sequence.startLine + 1);
571
- }
572
- catch (error) {
573
- vscode.window.showErrorMessage(`Failed to generate contract report: ${error}`);
574
- }
575
- });
576
- // Register the Contract Decoration provider for schema assertion status indicators
577
- const contractDecorationProvider = new contractDecorationProvider_1.ContractDecorationProvider();
578
- contractDecorationProviderInstance = contractDecorationProvider;
579
- // Register hover provider for schema assertion lines
580
- const contractHoverProvider = vscode.languages.registerHoverProvider({ language: 'norn' }, (0, contractDecorationProvider_1.createContractHoverProvider)());
581
- // Update diagnostics when document changes
582
- const onDidChangeDocument = vscode.workspace.onDidChangeTextDocument((event) => {
583
- if (event.document.languageId === 'norn' || event.document.languageId === 'nornenv' || event.document.languageId === 'nornapi') {
584
- diagnosticProvider.updateDiagnostics(event.document);
585
- }
586
- });
587
- // Update diagnostics when document opens
588
- const onDidOpenDocument = vscode.workspace.onDidOpenTextDocument((document) => {
589
- if (document.languageId === 'norn' || document.languageId === 'nornenv' || document.languageId === 'nornapi') {
590
- diagnosticProvider.updateDiagnostics(document);
591
- (0, environmentProvider_1.refreshEnvironmentStatusBar)();
592
- (0, environmentProvider_1.refreshCoverageStatusBarContext)();
593
- vscode.commands.executeCommand('norn.refreshCodeLenses');
594
- }
595
- });
596
- // Refresh environment context when switching active editors
597
- const onDidChangeActiveEditor = vscode.window.onDidChangeActiveTextEditor(() => {
598
- (0, environmentProvider_1.refreshEnvironmentStatusBar)();
599
- (0, environmentProvider_1.refreshCoverageStatusBarContext)();
600
- vscode.commands.executeCommand('norn.refreshCodeLenses');
601
- });
602
- // Clear diagnostics when document closes
603
- const onDidCloseDocument = vscode.workspace.onDidCloseTextDocument((document) => {
604
- diagnosticProvider.clearDiagnostics(document);
605
- });
606
- // Update diagnostics for already open documents
607
- vscode.workspace.textDocuments.forEach((document) => {
608
- if (document.languageId === 'norn' || document.languageId === 'nornenv' || document.languageId === 'nornapi') {
609
- diagnosticProvider.updateDiagnostics(document);
610
- }
611
- });
612
- context.subscriptions.push(sendRequestCommand, runSequenceCommand, clearCookiesCommand, showCookiesCommand, selectEnvironmentCommand, selectEnvironmentAndRefreshCommand, createEnvFileCommand, importSwaggerCommand, generateSchemasFromSwaggerCommand, refreshDiagnosticsCommand, openContractViewCommand, openSchemaFileCommand, validateSchemaAssertionNowCommand, viewContractReportCommand, contractDecorationProvider, contractHoverProvider, showCoverageCommand, refreshCoverageCommand, coverageUpdateListener, statusBarItem, coverageStatusBarItem, codeLensProvider, completionProvider, diagnosticProvider, onDidChangeDocument, onDidOpenDocument, onDidChangeActiveEditor, onDidCloseDocument, nornapiFileWatcher, documentSaveWatcher, { dispose: environmentProvider_1.disposeStatusBar });
613
- }
614
- function formatSequenceCaseLabel(caseParams, index) {
615
- const entries = Object.entries(caseParams);
616
- if (entries.length === 0) {
617
- return `Case ${index + 1}`;
618
- }
619
- const formatted = entries.map(([key, value]) => {
620
- if (typeof value === 'string') {
621
- return `${key}="${value}"`;
622
- }
623
- return `${key}=${value}`;
624
- });
625
- return `[${formatted.join(', ')}]`;
626
- }
627
- function normalizeTheoryCase(caseValue, sequenceParamNames) {
628
- if (caseValue && typeof caseValue === 'object' && !Array.isArray(caseValue)) {
629
- return caseValue;
630
- }
631
- if (sequenceParamNames.length === 1) {
632
- return { [sequenceParamNames[0]]: caseValue };
633
- }
634
- return null;
635
- }
636
- async function loadTheoryCasesFromSource(sourcePath, workingDir, sequence) {
637
- const resolvedPath = path.resolve(workingDir, sourcePath);
638
- const content = await fs.readFile(resolvedPath, 'utf-8');
639
- const parsed = JSON.parse(content);
640
- if (!Array.isArray(parsed)) {
641
- throw new Error(`Theory file "${sourcePath}" must contain a JSON array of test cases`);
642
- }
643
- const paramNames = sequence.parameters.map(p => p.name);
644
- const cases = [];
645
- for (let i = 0; i < parsed.length; i++) {
646
- const normalized = normalizeTheoryCase(parsed[i], paramNames);
647
- if (!normalized) {
648
- throw new Error(`Theory file "${sourcePath}" has invalid case at index ${i}. ` +
649
- `Use objects for multi-parameter sequences.`);
650
- }
651
- cases.push(normalized);
652
- }
653
- return cases;
654
- }
655
- async function resolveSequenceRunCases(sequence, workingDir, defaultArgs) {
656
- const inlineCases = sequence.theoryData?.cases || [];
657
- let finalCases = [];
658
- if (inlineCases.length > 0) {
659
- finalCases = inlineCases;
660
- }
661
- else if (sequence.theoryData?.source) {
662
- finalCases = await loadTheoryCasesFromSource(sequence.theoryData.source, workingDir, sequence);
663
- }
664
- if (finalCases.length === 0) {
665
- return [{ args: { ...defaultArgs } }];
666
- }
667
- return finalCases.map((caseParams, index) => {
668
- const args = { ...defaultArgs };
669
- for (const [key, value] of Object.entries(caseParams)) {
670
- if (value !== undefined) {
671
- args[key] = String(value);
672
- }
673
- }
674
- return {
675
- label: formatSequenceCaseLabel(caseParams, index),
676
- args
677
- };
678
- });
679
- }
680
- async function processSequence(extensionUri, lineFromCodeLens) {
681
- const editor = vscode.window.activeTextEditor;
682
- if (!editor) {
683
- vscode.window.showErrorMessage("No active editor");
684
- return;
685
- }
686
- const fullText = editor.document.getText();
687
- const targetLine = lineFromCodeLens ?? editor.selection.active.line;
688
- const sequence = (0, sequenceRunner_1.getSequenceAtLine)(fullText, targetLine);
689
- if (!sequence) {
690
- vscode.window.showErrorMessage("No sequence found at cursor position");
691
- return;
692
- }
693
- // Merge environment variables with file-level variables (outside sequences)
694
- // Variables inside sequences are local to that sequence
695
- const envVariables = (0, environmentProvider_1.getEnvironmentVariables)(editor.document.uri.fsPath);
696
- const fileVariables = (0, parser_1.extractFileLevelVariables)(fullText);
697
- const allVariables = { ...envVariables, ...fileVariables };
698
- // Apply default parameter values for sequences with all-optional parameters
699
- const defaultArgs = {};
700
- if (sequence.parameters) {
701
- for (const param of sequence.parameters) {
702
- if (param.defaultValue !== undefined) {
703
- defaultArgs[param.name] = param.defaultValue;
704
- }
705
- }
706
- }
707
- // Get working directory from the file's location
708
- const workingDir = path.dirname(editor.document.uri.fsPath);
709
- // Resolve imports to get the full document text with imported requests/sequences
710
- const importResult = await (0, parser_1.resolveImports)(fullText, workingDir, async (filePath) => {
711
- const content = await fs.readFile(filePath, 'utf-8');
712
- return content;
713
- });
714
- // Show import errors - duplicate errors block execution, others are warnings
715
- const duplicateErrors = importResult.errors.filter(err => err.error.includes('Duplicate header group') ||
716
- err.error.includes('Duplicate endpoint') ||
717
- err.error.includes('Duplicate named request') ||
718
- err.error.includes('Duplicate sequence'));
719
- const otherErrors = importResult.errors.filter(err => !err.error.includes('Duplicate header group') &&
720
- !err.error.includes('Duplicate endpoint') &&
721
- !err.error.includes('Duplicate named request') &&
722
- !err.error.includes('Duplicate sequence'));
723
- // Show non-duplicate errors as warnings
724
- for (const err of otherErrors) {
725
- vscode.window.showWarningMessage(`Import error: ${err.path} - ${err.error}`);
726
- }
727
- // Duplicate errors block execution
728
- if (duplicateErrors.length > 0) {
729
- vscode.window.showErrorMessage(`Cannot run sequence - duplicate definitions found:\n${duplicateErrors[0].error}`);
730
- return;
731
- }
732
- // Combine original text with imported content
733
- const fullTextWithImports = importResult.importedContent
734
- ? `${importResult.importedContent}\n\n${fullText}`
735
- : fullText;
736
- // Build API definitions from imports (header groups and endpoints from .nornapi files)
737
- const apiDefinitions = (importResult.headerGroups && importResult.headerGroups.length > 0) ||
738
- (importResult.endpoints && importResult.endpoints.length > 0)
739
- ? { headerGroups: importResult.headerGroups || [], endpoints: importResult.endpoints || [] }
740
- : undefined;
741
- let executedRuns = false;
742
- try {
743
- const runCases = await resolveSequenceRunCases(sequence, workingDir, defaultArgs);
744
- if (runCases.length <= 1) {
745
- // Single run: keep streaming behavior.
746
- const totalSteps = (0, sequenceRunner_1.countSequenceSteps)(sequence.content);
747
- responsePanel_1.ResponsePanel.startSequenceRun(extensionUri, sequence.name, totalSteps, editor.document.uri.fsPath);
748
- try {
749
- executedRuns = true;
750
- const runArgs = runCases[0]?.args ?? defaultArgs;
751
- const result = await (0, sequenceRunner_1.runSequence)(sequence.content, allVariables, workingDir, fullTextWithImports, (progressInfo) => {
752
- // Handle sequence start/end markers for grouping
753
- if (progressInfo.stepType === 'sequenceStart' || progressInfo.stepType === 'sequenceEnd') {
754
- responsePanel_1.ResponsePanel.addSequenceStep(undefined, progressInfo);
755
- }
756
- // Stream each step result to the panel as it completes
757
- else if (progressInfo.stepResult) {
758
- responsePanel_1.ResponsePanel.addSequenceStep(progressInfo.stepResult, progressInfo);
759
- }
760
- }, undefined, // callStack
761
- runArgs, apiDefinitions, // header groups and endpoints from .nornapi imports
762
- undefined, // tagFilterOptions
763
- importResult.sequenceSources // track source files for script path resolution
764
- );
765
- result.name = sequence.name;
766
- responsePanel_1.ResponsePanel.finalizeSequence(result);
767
- // Save validation results for schema assertions and refresh decorations
768
- // Add sequence.startLine + 1 to convert relative line numbers to absolute file line numbers
769
- // (+1 because the sequence content starts after the "sequence ..." declaration line)
770
- saveSchemaValidationResults(result, editor.document.uri.fsPath, sequence.startLine + 1);
771
- }
772
- catch (error) {
773
- const errorMessage = error instanceof Error ? error.message : String(error);
774
- vscode.window.showErrorMessage(`Sequence failed: ${errorMessage}`);
775
- // Finalize with error state
776
- responsePanel_1.ResponsePanel.finalizeSequence({
777
- name: sequence.name,
778
- success: false,
779
- responses: [],
780
- scriptResults: [],
781
- assertionResults: [],
782
- steps: [],
783
- errors: [errorMessage],
784
- duration: 0
785
- });
786
- }
787
- }
788
- else {
789
- // Parameterized runs: execute all cases and show combined report in one panel.
790
- const caseResults = [];
791
- await vscode.window.withProgress({
792
- location: vscode.ProgressLocation.Notification,
793
- title: `Running ${sequence.name} (${runCases.length} cases)`,
794
- cancellable: false
795
- }, async (progress) => {
796
- for (let i = 0; i < runCases.length; i++) {
797
- const runCase = runCases[i];
798
- const runLabel = runCase.label || `Case ${i + 1}`;
799
- progress.report({
800
- message: runLabel,
801
- increment: 100 / runCases.length
802
- });
803
- try {
804
- executedRuns = true;
805
- const result = await (0, sequenceRunner_1.runSequence)(sequence.content, allVariables, workingDir, fullTextWithImports, undefined, // no per-step streaming in multi-run mode
806
- undefined, // callStack
807
- runCase.args, apiDefinitions, undefined, importResult.sequenceSources);
808
- result.name = sequence.name;
809
- saveSchemaValidationResults(result, editor.document.uri.fsPath, sequence.startLine + 1);
810
- caseResults.push({ label: runLabel, result });
811
- }
812
- catch (error) {
813
- const errorMessage = error instanceof Error ? error.message : String(error);
814
- caseResults.push({
815
- label: runLabel,
816
- result: {
817
- name: sequence.name,
818
- success: false,
819
- responses: [],
820
- scriptResults: [],
821
- assertionResults: [],
822
- steps: [],
823
- errors: [errorMessage],
824
- duration: 0
825
- }
826
- });
827
- }
828
- }
829
- });
830
- responsePanel_1.ResponsePanel.showMultiSequenceResult(extensionUri, sequence.name, caseResults);
831
- }
832
- if (executedRuns) {
833
- // Recalculate coverage after sequence execution (uses cached swagger specs)
834
- (0, coverageCalculator_1.recalculateCoverageAfterExecution)().catch(() => {
835
- // Silently ignore coverage calculation errors
836
- });
837
- }
838
- }
839
- catch (error) {
840
- const errorMessage = error instanceof Error ? error.message : String(error);
841
- vscode.window.showErrorMessage(`Sequence failed: ${errorMessage}`);
842
- }
843
- }
844
- /**
845
- * Save schema validation results to cache and refresh decorations
846
- */
847
- function saveSchemaValidationResults(result, sourceFile, lineOffset = 0) {
848
- try {
849
- // Find all matchesSchema assertion steps
850
- for (const step of result.steps) {
851
- if (step.type === 'assertion' && step.assertion?.operator === 'matchesSchema') {
852
- const assertion = step.assertion;
853
- const schemaPath = assertion.schemaPath || assertion.rightValue || '';
854
- const status = assertion.passed ? 'pass' : 'fail';
855
- const errorCount = assertion.schemaErrors?.length || 0;
856
- const errorSummary = assertion.schemaErrors?.[0]?.message || assertion.error?.substring(0, 100);
857
- // Only cache if we have a line number
858
- if (step.lineNumber !== undefined) {
859
- // Convert relative line number to absolute file line number using lineOffset
860
- const absoluteLine = step.lineNumber + lineOffset;
861
- (0, validationCache_1.saveValidationResult)({
862
- schemaPath,
863
- sourceFile,
864
- assertionLine: absoluteLine,
865
- status,
866
- lastRunTime: new Date().toISOString(),
867
- errorCount: status === 'fail' ? errorCount : undefined,
868
- errorSummary: status === 'fail' ? errorSummary : undefined
869
- });
870
- }
871
- }
872
- }
873
- // Refresh decorations
874
- contractDecorationProviderInstance?.refreshAllDecorations();
875
- }
876
- catch (e) {
877
- // Silently ignore cache errors
878
- console.error('Failed to save validation results:', e);
879
- }
880
- }
881
- async function processEditorsInput(extensionUri, lineFromCodeLens) {
882
- const editor = vscode.window.activeTextEditor;
883
- if (!editor) {
884
- vscode.window.showErrorMessage("No active editor");
885
- return;
886
- }
887
- const fullText = editor.document.getText();
888
- const selection = editor.selection;
889
- let requestText;
890
- if (!selection.isEmpty) {
891
- // User selected specific text
892
- requestText = editor.document.getText(selection);
893
- }
894
- else {
895
- // Use line from CodeLens if provided, otherwise use cursor position
896
- const targetLine = lineFromCodeLens ?? selection.active.line;
897
- requestText = (0, parser_1.getRequestBlockAtLine)(fullText, targetLine);
898
- }
899
- // Get working directory from the file's location
900
- const workingDir = path.dirname(editor.document.uri.fsPath);
901
- try {
902
- // Extract variables: environment + file (file takes precedence)
903
- const envVariables = (0, environmentProvider_1.getEnvironmentVariables)(editor.document.uri.fsPath);
904
- const fileVariables = (0, parser_1.extractVariables)(fullText);
905
- const variables = { ...envVariables, ...fileVariables };
906
- // Resolve imports to get API definitions (endpoints and header groups)
907
- const importResult = await (0, parser_1.resolveImports)(fullText, workingDir, async (filePath) => {
908
- const content = await fs.readFile(filePath, 'utf-8');
909
- return content;
910
- });
911
- // Check for duplicate definition errors which should block execution
912
- const duplicateErrors = importResult.errors.filter(err => err.error.includes('Duplicate header group') ||
913
- err.error.includes('Duplicate endpoint') ||
914
- err.error.includes('Duplicate named request') ||
915
- err.error.includes('Duplicate sequence'));
916
- if (duplicateErrors.length > 0) {
917
- vscode.window.showErrorMessage(`Cannot run request - duplicate definitions found:\n${duplicateErrors[0].error}`);
918
- return;
919
- }
920
- // Build API definitions from imports
921
- const apiDefinitions = (importResult.headerGroups && importResult.headerGroups.length > 0) ||
922
- (importResult.endpoints && importResult.endpoints.length > 0)
923
- ? { headerGroups: importResult.headerGroups || [], endpoints: importResult.endpoints || [] }
924
- : undefined;
925
- let parsed;
926
- let requestDescription;
927
- // Check if this is an API request (uses endpoint syntax like GET GetPetById(1))
928
- if (apiDefinitions &&
929
- apiDefinitions.endpoints.length > 0 &&
930
- (0, nornapiParser_1.isApiRequestLine)(requestText, apiDefinitions.endpoints)) {
931
- const apiRequest = (0, nornapiParser_1.parseApiRequest)(requestText, apiDefinitions.endpoints, apiDefinitions.headerGroups);
932
- if (apiRequest) {
933
- const endpoint = (0, nornapiParser_1.getEndpoint)({ headerGroups: apiDefinitions.headerGroups, endpoints: apiDefinitions.endpoints }, apiRequest.endpointName);
934
- if (endpoint) {
935
- // Substitute {{variable}} references in the endpoint path
936
- let resolvedPath = endpoint.path;
937
- for (const [key, value] of Object.entries(variables)) {
938
- resolvedPath = resolvedPath.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(value));
939
- }
940
- // Substitute {param} placeholders with the values passed to the endpoint
941
- for (const [paramName, paramValue] of Object.entries(apiRequest.params)) {
942
- // Resolve variable references in param values
943
- let resolvedValue = paramValue;
944
- if (variables[paramValue] !== undefined) {
945
- resolvedValue = String(variables[paramValue]);
946
- }
947
- else if (paramValue.startsWith('{{') && paramValue.endsWith('}}')) {
948
- const varName = paramValue.slice(2, -2);
949
- if (variables[varName] !== undefined) {
950
- resolvedValue = String(variables[varName]);
951
- }
952
- }
953
- resolvedPath = resolvedPath.replace(`{${paramName}}`, resolvedValue);
954
- }
955
- // Collect headers from header groups
956
- const combinedHeaders = {};
957
- for (const groupName of apiRequest.headerGroupNames) {
958
- const group = apiDefinitions.headerGroups.find(hg => hg.name === groupName);
959
- if (group) {
960
- const resolvedHeaders = (0, nornapiParser_1.resolveHeaderValues)(group, variables);
961
- Object.assign(combinedHeaders, resolvedHeaders);
962
- }
963
- }
964
- // Add inline headers (these take precedence over header groups)
965
- for (const [headerName, headerValue] of Object.entries(apiRequest.inlineHeaders)) {
966
- // Resolve variable references in header values
967
- let resolved = headerValue;
968
- resolved = resolved.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (_, varName) => {
969
- return variables[varName] !== undefined ? String(variables[varName]) : `{{${varName}}}`;
970
- });
971
- combinedHeaders[headerName] = resolved;
972
- }
973
- // Parse the request text to extract body (if any)
974
- const parsedForBody = (0, parser_1.parserHttpRequest)(requestText, variables);
975
- parsed = {
976
- method: apiRequest.method,
977
- url: resolvedPath,
978
- headers: combinedHeaders,
979
- body: parsedForBody.body
980
- };
981
- requestDescription = `${apiRequest.endpointName}(${Object.values(apiRequest.params).join(', ')}) → ${parsed.method} ${parsed.url}`;
982
- }
983
- else {
984
- throw new Error(`Unknown endpoint: ${apiRequest.endpointName}`);
985
- }
986
- }
987
- else {
988
- // Fallback to regular parsing
989
- parsed = (0, parser_1.parserHttpRequest)(requestText, variables);
990
- requestDescription = `${parsed.method} ${parsed.url}`;
991
- }
992
- }
993
- else {
994
- // Regular HTTP request
995
- parsed = (0, parser_1.parserHttpRequest)(requestText, variables);
996
- requestDescription = `${parsed.method} ${parsed.url}`;
997
- // Check for header group names in the request text
998
- // This allows: GET https://example.com Json Auth
999
- // Or: GET https://example.com
1000
- // Json
1001
- // Auth
1002
- if (apiDefinitions && apiDefinitions.headerGroups.length > 0) {
1003
- const requestLines = requestText.split('\n');
1004
- const headerGroupNames = apiDefinitions.headerGroups.map(hg => hg.name);
1005
- const foundGroups = [];
1006
- for (const line of requestLines) {
1007
- const trimmed = line.trim();
1008
- // Skip empty lines, comments, variable lines
1009
- if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('var ')) {
1010
- continue;
1011
- }
1012
- // Check if this is the HTTP method line - look for header groups at the end
1013
- const methodMatch = trimmed.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(.+)$/i);
1014
- if (methodMatch) {
1015
- const afterMethod = methodMatch[2];
1016
- const tokens = afterMethod.split(/\s+/);
1017
- // Scan from the end to find header groups
1018
- for (let i = tokens.length - 1; i >= 0; i--) {
1019
- if (headerGroupNames.includes(tokens[i])) {
1020
- foundGroups.push(tokens[i]);
1021
- }
1022
- else {
1023
- break;
1024
- }
1025
- }
1026
- continue;
1027
- }
1028
- // Skip lines that look like headers (Name: Value)
1029
- if (/^[A-Za-z0-9\-_]+\s*:\s*.+$/.test(trimmed)) {
1030
- continue;
1031
- }
1032
- // Check if this line contains header group names (can be multiple space-separated)
1033
- const potentialGroups = trimmed.split(/\s+/);
1034
- for (const groupName of potentialGroups) {
1035
- if (headerGroupNames.includes(groupName)) {
1036
- foundGroups.push(groupName);
1037
- }
1038
- }
1039
- }
1040
- // Collect headers from all found groups and strip from URL
1041
- for (const groupName of foundGroups) {
1042
- const group = apiDefinitions.headerGroups.find(hg => hg.name === groupName);
1043
- if (group) {
1044
- const resolvedHeaders = (0, nornapiParser_1.resolveHeaderValues)(group, variables);
1045
- parsed.headers = { ...resolvedHeaders, ...parsed.headers };
1046
- }
1047
- // Strip from URL
1048
- const endPattern = new RegExp(`\\s+${groupName}$`);
1049
- parsed.url = parsed.url.replace(endPattern, '');
1050
- }
1051
- parsed.url = parsed.url.trim();
1052
- requestDescription = `${parsed.method} ${parsed.url}`;
1053
- }
1054
- }
1055
- // Show loading indicator
1056
- await vscode.window.withProgress({
1057
- location: vscode.ProgressLocation.Notification,
1058
- title: `Sending ${parsed.method} request...`,
1059
- cancellable: false
1060
- }, async () => {
1061
- try {
1062
- const response = await (0, httpClient_1.sendRequest)(parsed);
1063
- responsePanel_1.ResponsePanel.show(extensionUri, parsed.method, parsed.url, response);
1064
- }
1065
- catch (error) {
1066
- responsePanel_1.ResponsePanel.showError(extensionUri, parsed.method, parsed.url, error.message);
1067
- }
1068
- });
1069
- }
1070
- catch (error) {
1071
- vscode.window.showErrorMessage(error.message);
1072
- }
1073
- }
1074
- /**
1075
- * Simple debounce helper
1076
- */
1077
- function debounce(fn, delay) {
1078
- let timeoutId;
1079
- return ((...args) => {
1080
- if (timeoutId) {
1081
- clearTimeout(timeoutId);
1082
- }
1083
- timeoutId = setTimeout(() => {
1084
- fn(...args);
1085
- timeoutId = undefined;
1086
- }, delay);
1087
- });
1088
- }
1089
- function extractSwaggerUrlsFromNornapi(content) {
1090
- const urls = new Set();
1091
- const lines = content.split('\n');
1092
- for (const line of lines) {
1093
- const trimmed = line.trim();
1094
- if (!trimmed || trimmed.startsWith('#')) {
1095
- continue;
1096
- }
1097
- const quotedMatch = trimmed.match(/^swagger\s+["']([^"']+)["']\s*$/i);
1098
- if (quotedMatch) {
1099
- urls.add(quotedMatch[1]);
1100
- continue;
1101
- }
1102
- const unquotedMatch = trimmed.match(/^swagger\s+(https?:\/\/\S+)\s*$/i);
1103
- if (unquotedMatch) {
1104
- urls.add(unquotedMatch[1]);
1105
- }
1106
- }
1107
- return Array.from(urls);
1108
- }
1109
- function deactivate() {
1110
- // Clear caches on deactivation
1111
- (0, swaggerParser_1.clearSwaggerCache)();
1112
- (0, coverageCalculator_1.clearCoverageCache)();
1113
- }
1114
- //# sourceMappingURL=extension.js.map