norn-cli 2.2.2 → 2.4.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/.claude/settings.local.json +18 -0
- package/.claude/skills/norn-social-campaign/SKILL.md +70 -0
- package/CHANGELOG.md +22 -1
- package/LICENSE +20 -29
- package/README.md +32 -1
- package/demos/nornenv-region-refactor/README.md +64 -0
- package/demos/nornenv-showcase/README.md +62 -0
- package/demos/nornenv-showcase/norn.config.json +16 -0
- package/demos/nornenv-showcase/showcase.norn +70 -0
- package/demos/nornenv-showcase/showcase.nornapi +26 -0
- package/demos/nornenv-showcase/showcase.nornsql +20 -0
- package/dist/cli.js +564 -54
- package/out/apiResponseIntellisenseCache.js +394 -0
- package/out/assertionRunner.js +567 -0
- package/out/cacheDir.js +136 -0
- package/out/chatParticipant.js +763 -0
- package/out/cli/colors.js +127 -0
- package/out/cli/formatters/assertion.js +102 -0
- package/out/cli/formatters/index.js +23 -0
- package/out/cli/formatters/response.js +106 -0
- package/out/cli/formatters/summary.js +246 -0
- package/out/cli/redaction.js +237 -0
- package/out/cli/reporters/html.js +689 -0
- package/out/cli/reporters/index.js +22 -0
- package/out/cli/reporters/junit.js +226 -0
- package/out/codeLensProvider.js +351 -0
- package/out/compareContentProvider.js +85 -0
- package/out/completionProvider.js +3739 -0
- package/out/contractAssertionSummary.js +225 -0
- package/out/contractDecorationProvider.js +243 -0
- package/out/coverageCalculator.js +879 -0
- package/out/coveragePanel.js +597 -0
- package/out/debug/breakpointResolver.js +84 -0
- package/out/debug/breakpoints.js +52 -0
- package/out/debug/nornDebugAdapter.js +166 -0
- package/out/debug/nornDebugSession.js +613 -0
- package/out/debug/sequenceLocationIndex.js +77 -0
- package/out/debug/types.js +3 -0
- package/out/deepClone.js +21 -0
- package/out/diagnosticProvider.js +2554 -0
- package/out/environmentParser.js +736 -0
- package/out/environmentProvider.js +544 -0
- package/out/environmentTemplates.js +146 -0
- package/out/errors/formatError.js +113 -0
- package/out/errors/nornError.js +29 -0
- package/out/formUrlEncoded.js +89 -0
- package/out/httpClient.js +348 -0
- package/out/httpRuntimeOptions.js +16 -0
- package/out/importErrors.js +31 -0
- package/out/inlayHintResolver.js +70 -0
- package/out/jsonFileReader.js +323 -0
- package/out/mcpClient.js +193 -0
- package/out/mcpConfig.js +184 -0
- package/out/mcpToolIntellisenseCache.js +96 -0
- package/out/mcpToolSchema.js +50 -0
- package/out/nornConfig.js +132 -0
- package/out/nornHoverProvider.js +124 -0
- package/out/nornInlayHintsProvider.js +191 -0
- package/out/nornPrompt.js +755 -0
- package/out/nornSqlParser.js +286 -0
- package/out/nornapiHoverProvider.js +135 -0
- package/out/nornapiInlayHintsProvider.js +94 -0
- package/out/nornapiParser.js +324 -0
- package/out/nornenvCodeActionProvider.js +101 -0
- package/out/nornenvDecorationProvider.js +239 -0
- package/out/nornenvFoldingProvider.js +63 -0
- package/out/nornenvHoverProvider.js +114 -0
- package/out/nornenvInlayHintsProvider.js +99 -0
- package/out/nornenvLanguageModel.js +187 -0
- package/out/nornenvRegionRefactor.js +267 -0
- package/out/nornsqlHoverProvider.js +95 -0
- package/out/nornsqlInlayHintsProvider.js +114 -0
- package/out/parser.js +839 -0
- package/out/pathAccess.js +28 -0
- package/out/postmanImportPanel.js +732 -0
- package/out/postmanImportPlanner.js +1155 -0
- package/out/postmanImportSidebarView.js +532 -0
- package/out/quotedString.js +35 -0
- package/out/requestPreparation.js +179 -0
- package/out/requestValidation.js +146 -0
- package/out/responsePanel.js +7754 -0
- package/out/schemaGenerator.js +562 -0
- package/out/scriptRunner.js +419 -0
- package/out/secrets/cliSecrets.js +415 -0
- package/out/secrets/crypto.js +105 -0
- package/out/secrets/envFileSecrets.js +177 -0
- package/out/secrets/keyStore.js +259 -0
- package/out/sequenceDeclaration.js +15 -0
- package/out/sequenceRunner.js +3590 -0
- package/out/sqlAdapterRunner.js +122 -0
- package/out/sqlBuiltInAdapters.js +604 -0
- package/out/sqlConfig.js +184 -0
- package/out/starterCatalog.js +554 -0
- package/out/stringUtils.js +25 -0
- package/out/swaggerBodyIntellisenseCache.js +114 -0
- package/out/swaggerParser.js +464 -0
- package/out/testProvider.js +767 -0
- package/out/theoryCaseLoader.js +113 -0
- package/out/validationCache.js +211 -0
- package/package.json +38 -11
- package/.kanbn/index.md +0 -31
- package/.kanbn/tasks/book-first-mentor-session.md +0 -13
- package/.kanbn/tasks/decide-what-success-in-a-pilot-looks-like.md +0 -9
- package/.kanbn/tasks/do-5-customer-conversations.md +0 -9
- package/.kanbn/tasks/finalise-the-one-line-pitch.md +0 -11
- package/.kanbn/tasks/interview-script.md +0 -49
- package/.kanbn/tasks/make-a-list-of-10-people-to-speak-to.md +0 -11
- package/.kanbn/tasks/prepare-your-customer-interview-questions.md +0 -11
- package/.kanbn/tasks/recruit-2/342/200/2233-pilot-users.md +0 -9
- package/.kanbn/tasks/refine-your-pitch.md +0 -9
- package/.kanbn/tasks/use-the-shiplight-website-as-a-template-to-improve-norn-website.md +0 -9
- package/.kanbn/tasks/write-down-repeated-wording.md +0 -9
- package/.kanbn/tasks/write-the-one-pager.md +0 -27
package/out/parser.js
ADDED
|
@@ -0,0 +1,839 @@
|
|
|
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.extractRetryOptions = extractRetryOptions;
|
|
37
|
+
exports.extractNamedRequests = extractNamedRequests;
|
|
38
|
+
exports.getNamedRequest = getNamedRequest;
|
|
39
|
+
exports.extractVariables = extractVariables;
|
|
40
|
+
exports.extractFileLevelVariables = extractFileLevelVariables;
|
|
41
|
+
exports.attachEnvironmentScope = attachEnvironmentScope;
|
|
42
|
+
exports.copyEnvironmentScope = copyEnvironmentScope;
|
|
43
|
+
exports.substituteVariables = substituteVariables;
|
|
44
|
+
exports.parserHttpRequest = parserHttpRequest;
|
|
45
|
+
exports.getRequestBlockAtLine = getRequestBlockAtLine;
|
|
46
|
+
exports.extractImports = extractImports;
|
|
47
|
+
exports.resolveImports = resolveImports;
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const nornapiParser_1 = require("./nornapiParser");
|
|
50
|
+
const nornSqlParser_1 = require("./nornSqlParser");
|
|
51
|
+
const pathAccess_1 = require("./pathAccess");
|
|
52
|
+
const stringUtils_1 = require("./stringUtils");
|
|
53
|
+
/**
|
|
54
|
+
* Extract retry and backoff options from a request line.
|
|
55
|
+
* Syntax: GET "URL" retry 3 backoff 500 ms
|
|
56
|
+
* Returns the cleaned line (without retry/backoff) and the parsed options.
|
|
57
|
+
*/
|
|
58
|
+
function extractRetryOptions(line) {
|
|
59
|
+
let cleanedLine = line;
|
|
60
|
+
let retryCount;
|
|
61
|
+
let backoffMs;
|
|
62
|
+
// Extract retry count: "retry 3" or "retry 3 "
|
|
63
|
+
const retryMatch = line.match(/\bretry\s+(\d+)\b/i);
|
|
64
|
+
if (retryMatch) {
|
|
65
|
+
retryCount = parseInt(retryMatch[1], 10);
|
|
66
|
+
cleanedLine = cleanedLine.replace(retryMatch[0], '').trim();
|
|
67
|
+
}
|
|
68
|
+
// Extract backoff: "backoff 500 ms" or "backoff 2s" or "backoff 1000"
|
|
69
|
+
const backoffMatch = line.match(/\bbackoff\s+(\d+(?:\.\d+)?)\s*(s|ms|seconds?|milliseconds?)?\b/i);
|
|
70
|
+
if (backoffMatch) {
|
|
71
|
+
const value = parseFloat(backoffMatch[1]);
|
|
72
|
+
const unit = (backoffMatch[2] || 'ms').toLowerCase();
|
|
73
|
+
if (unit === 's' || unit.startsWith('second')) {
|
|
74
|
+
backoffMs = value * 1000;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
backoffMs = value;
|
|
78
|
+
}
|
|
79
|
+
cleanedLine = cleanedLine.replace(backoffMatch[0], '').trim();
|
|
80
|
+
}
|
|
81
|
+
// Default backoff if retry specified but not backoff
|
|
82
|
+
if (retryCount !== undefined && backoffMs === undefined) {
|
|
83
|
+
backoffMs = 1000; // Default 1 second
|
|
84
|
+
}
|
|
85
|
+
return { cleanedLine, retryCount, backoffMs };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Extracts all named requests from the document.
|
|
89
|
+
* Named requests are declared as:
|
|
90
|
+
* [RequestName] (no spaces allowed)
|
|
91
|
+
* or legacy: [Name: RequestName]
|
|
92
|
+
* HTTP_METHOD URL
|
|
93
|
+
* ...
|
|
94
|
+
*/
|
|
95
|
+
function extractNamedRequests(text) {
|
|
96
|
+
const lines = text.split('\n');
|
|
97
|
+
const namedRequests = [];
|
|
98
|
+
// Match [SomeName] or [Name: SomeName] - name must not contain spaces
|
|
99
|
+
const nameRegex = /^\[(?:Name:\s*)?([a-zA-Z_][a-zA-Z0-9_-]*)\]$/;
|
|
100
|
+
const methodRegex = /^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+/i;
|
|
101
|
+
for (let i = 0; i < lines.length; i++) {
|
|
102
|
+
const line = lines[i].trim();
|
|
103
|
+
const nameMatch = line.match(nameRegex);
|
|
104
|
+
if (nameMatch) {
|
|
105
|
+
const name = nameMatch[1].trim();
|
|
106
|
+
const contentStartLine = i + 1;
|
|
107
|
+
// Find the end of this request (next [Name], sequence, ###, or end of file)
|
|
108
|
+
let endLine = lines.length - 1;
|
|
109
|
+
for (let j = contentStartLine; j < lines.length; j++) {
|
|
110
|
+
const scanLine = lines[j].trim();
|
|
111
|
+
if (isNamedRequestDeclarationLine(scanLine) ||
|
|
112
|
+
isSequenceStartDeclaration(scanLine) ||
|
|
113
|
+
isSequenceEndDeclaration(scanLine) ||
|
|
114
|
+
scanLine.startsWith('###')) {
|
|
115
|
+
endLine = j - 1;
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Trim trailing empty lines
|
|
120
|
+
while (endLine > contentStartLine && lines[endLine].trim() === '') {
|
|
121
|
+
endLine--;
|
|
122
|
+
}
|
|
123
|
+
const content = lines.slice(contentStartLine, endLine + 1).join('\n');
|
|
124
|
+
// Only add if there's actual content with an HTTP method
|
|
125
|
+
if (content.split('\n').some(l => methodRegex.test(l.trim()))) {
|
|
126
|
+
namedRequests.push({
|
|
127
|
+
name,
|
|
128
|
+
content,
|
|
129
|
+
startLine: i,
|
|
130
|
+
endLine
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
// Skip to end of this request
|
|
134
|
+
i = endLine;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return namedRequests;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Gets a named request by its name
|
|
141
|
+
*/
|
|
142
|
+
function getNamedRequest(text, name) {
|
|
143
|
+
const requests = extractNamedRequests(text);
|
|
144
|
+
return requests.find(r => r.name === name ||
|
|
145
|
+
r.name.toLowerCase() === name.toLowerCase());
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Patterns for runtime-computed variable values that should not be treated as static vars.
|
|
149
|
+
*/
|
|
150
|
+
const RUNTIME_VARIABLE_VALUE_PATTERNS = [
|
|
151
|
+
/^run\s+/i, // var x = run ... (scripts/sequences)
|
|
152
|
+
/^\$\d+/, // var x = $1... (response captures)
|
|
153
|
+
/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+/i // var x = METHOD url (request captures)
|
|
154
|
+
];
|
|
155
|
+
function isRuntimeComputedVariableValue(value) {
|
|
156
|
+
return RUNTIME_VARIABLE_VALUE_PATTERNS.some(pattern => pattern.test(value));
|
|
157
|
+
}
|
|
158
|
+
function isSequenceStartDeclaration(line) {
|
|
159
|
+
return /^(?:test\s+)?sequence\s+[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*\([^)]*\))?\s*$/i.test(line);
|
|
160
|
+
}
|
|
161
|
+
function isSequenceEndDeclaration(line) {
|
|
162
|
+
return /^end\s+sequence\s*$/i.test(line);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Extracts all STATIC variables from the document.
|
|
166
|
+
* Variables are declared as: var variableName = value
|
|
167
|
+
*
|
|
168
|
+
* Does NOT extract runtime-computed values:
|
|
169
|
+
* - var x = run ... (script commands)
|
|
170
|
+
* - var x = $1... (response captures)
|
|
171
|
+
* - var x = GET/POST/... url (request captures)
|
|
172
|
+
*/
|
|
173
|
+
function extractVariables(text) {
|
|
174
|
+
const variables = {};
|
|
175
|
+
// Allow optional leading whitespace for indented variables (inside sequences)
|
|
176
|
+
const variableRegex = /^\s*var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/gm;
|
|
177
|
+
let match;
|
|
178
|
+
while ((match = variableRegex.exec(text)) !== null) {
|
|
179
|
+
let value = match[2].trim();
|
|
180
|
+
// Skip runtime-computed values
|
|
181
|
+
const isRuntimeValue = isRuntimeComputedVariableValue(value);
|
|
182
|
+
if (isRuntimeValue) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
// Strip quotes from string literals
|
|
186
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
187
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
188
|
+
value = value.slice(1, -1);
|
|
189
|
+
}
|
|
190
|
+
variables[match[1]] = value;
|
|
191
|
+
}
|
|
192
|
+
return variables;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Extracts only file-level variables (outside sequences).
|
|
196
|
+
* Used for diagnostics to understand variable scope.
|
|
197
|
+
*/
|
|
198
|
+
function extractFileLevelVariables(text) {
|
|
199
|
+
const variables = {};
|
|
200
|
+
const lines = text.split('\n');
|
|
201
|
+
const variableRegex = /^\s*var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/;
|
|
202
|
+
// Use depth so malformed files don't accidentally leak inner vars to file scope.
|
|
203
|
+
let sequenceDepth = 0;
|
|
204
|
+
for (const line of lines) {
|
|
205
|
+
const trimmed = line.trim();
|
|
206
|
+
// Track sequence boundaries
|
|
207
|
+
if (isSequenceStartDeclaration(trimmed)) {
|
|
208
|
+
sequenceDepth++;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (isSequenceEndDeclaration(trimmed)) {
|
|
212
|
+
sequenceDepth = Math.max(0, sequenceDepth - 1);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
// Only extract variables outside sequences
|
|
216
|
+
if (sequenceDepth === 0) {
|
|
217
|
+
const match = line.match(variableRegex);
|
|
218
|
+
if (match) {
|
|
219
|
+
let value = match[2].trim();
|
|
220
|
+
// Skip runtime-computed values
|
|
221
|
+
if (isRuntimeComputedVariableValue(value)) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
// Strip quotes from string literals
|
|
225
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
226
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
227
|
+
value = value.slice(1, -1);
|
|
228
|
+
}
|
|
229
|
+
variables[match[1]] = value;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return variables;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Gets a value from an object using a dot-notation path.
|
|
237
|
+
* Supports array indexing with brackets: obj.array[0].property
|
|
238
|
+
*/
|
|
239
|
+
function getNestedValue(obj, path) {
|
|
240
|
+
return (0, pathAccess_1.getNestedPathValue)(obj, path);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Converts a value to a string for use in variable substitution.
|
|
244
|
+
*/
|
|
245
|
+
function valueToString(value) {
|
|
246
|
+
if (value === null) {
|
|
247
|
+
return 'null';
|
|
248
|
+
}
|
|
249
|
+
if (value === undefined) {
|
|
250
|
+
return '';
|
|
251
|
+
}
|
|
252
|
+
if (typeof value === 'object') {
|
|
253
|
+
return JSON.stringify(value);
|
|
254
|
+
}
|
|
255
|
+
return String(value);
|
|
256
|
+
}
|
|
257
|
+
const ENV_SCOPE_KEY = '$env';
|
|
258
|
+
function attachEnvironmentScope(variables, envVariables) {
|
|
259
|
+
if (!envVariables) {
|
|
260
|
+
return variables;
|
|
261
|
+
}
|
|
262
|
+
Object.defineProperty(variables, ENV_SCOPE_KEY, {
|
|
263
|
+
value: envVariables,
|
|
264
|
+
enumerable: false,
|
|
265
|
+
configurable: true,
|
|
266
|
+
writable: false
|
|
267
|
+
});
|
|
268
|
+
return variables;
|
|
269
|
+
}
|
|
270
|
+
function copyEnvironmentScope(target, source) {
|
|
271
|
+
const envScope = source[ENV_SCOPE_KEY];
|
|
272
|
+
if (envScope && typeof envScope === 'object') {
|
|
273
|
+
attachEnvironmentScope(target, envScope);
|
|
274
|
+
}
|
|
275
|
+
return target;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Substitutes variables in text.
|
|
279
|
+
* Variables are referenced as: {{variableName}} or {{variableName.property.path}}
|
|
280
|
+
* Supports nested property access for JSON objects stored as strings.
|
|
281
|
+
* Also supports object values directly (e.g., from var user = GET responses).
|
|
282
|
+
* Also supports $N response references (e.g., {{$1.status}}, {{$2.body.id}})
|
|
283
|
+
* Also supports explicit environment references (e.g., {{$env.baseUrl}})
|
|
284
|
+
*/
|
|
285
|
+
function substituteVariables(text, variables) {
|
|
286
|
+
// Match {{varName}} or {{varName.path.to.property}} or {{varName[0].property}}
|
|
287
|
+
// Also match {{$N}} / {{$N.path}} and {{$env.name}}
|
|
288
|
+
return text.replace(/\{\{(\$env|\$\d+|[a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_-]*|\[\d+\])*)\}\}/g, (match, varName, pathPart) => {
|
|
289
|
+
if (!(varName in variables)) {
|
|
290
|
+
return match; // Variable not found, return original
|
|
291
|
+
}
|
|
292
|
+
const value = variables[varName];
|
|
293
|
+
// If value is already an object (e.g., HttpResponse), navigate directly
|
|
294
|
+
if (typeof value === 'object' && value !== null) {
|
|
295
|
+
if (pathPart) {
|
|
296
|
+
const path = pathPart.replace(/^\./, ''); // Remove leading dot
|
|
297
|
+
const nestedValue = getNestedValue(value, path);
|
|
298
|
+
if (nestedValue === undefined && varName === ENV_SCOPE_KEY) {
|
|
299
|
+
return match;
|
|
300
|
+
}
|
|
301
|
+
return valueToString(nestedValue);
|
|
302
|
+
}
|
|
303
|
+
if (varName === ENV_SCOPE_KEY) {
|
|
304
|
+
return match;
|
|
305
|
+
}
|
|
306
|
+
// No path, stringify the whole object
|
|
307
|
+
return valueToString(value);
|
|
308
|
+
}
|
|
309
|
+
// If there's a path and value is a string, try to parse as JSON and navigate
|
|
310
|
+
if (pathPart && typeof value === 'string') {
|
|
311
|
+
try {
|
|
312
|
+
const parsed = JSON.parse(value);
|
|
313
|
+
const path = pathPart.replace(/^\./, ''); // Remove leading dot
|
|
314
|
+
const nestedValue = getNestedValue(parsed, path);
|
|
315
|
+
if (nestedValue === undefined && varName === ENV_SCOPE_KEY) {
|
|
316
|
+
return match;
|
|
317
|
+
}
|
|
318
|
+
return valueToString(nestedValue);
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
if (varName === ENV_SCOPE_KEY) {
|
|
322
|
+
return match;
|
|
323
|
+
}
|
|
324
|
+
// Not valid JSON, return original value
|
|
325
|
+
return value;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// Simple substitution
|
|
329
|
+
return String(value);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Extracts a single request block from the text.
|
|
334
|
+
* Stops parsing at ### delimiter (REST Client style separator).
|
|
335
|
+
* A blank line separates headers from body (standard HTTP convention).
|
|
336
|
+
*/
|
|
337
|
+
function parserHttpRequest(text, variables = {}) {
|
|
338
|
+
// Substitute variables first
|
|
339
|
+
const substitutedText = substituteVariables(text, variables);
|
|
340
|
+
// Split by ### to get only the first request block
|
|
341
|
+
const requestBlock = substitutedText.split(/^###/m)[0];
|
|
342
|
+
const allLines = requestBlock.split('\n');
|
|
343
|
+
// Find the first line that looks like a request (METHOD URL)
|
|
344
|
+
const methodRegex = /^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+/i;
|
|
345
|
+
let requestLineIndex = -1;
|
|
346
|
+
for (let i = 0; i < allLines.length; i++) {
|
|
347
|
+
const trimmed = allLines[i].trim();
|
|
348
|
+
if (methodRegex.test(trimmed)) {
|
|
349
|
+
requestLineIndex = i;
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (requestLineIndex === -1) {
|
|
354
|
+
throw new Error('No valid HTTP method found');
|
|
355
|
+
}
|
|
356
|
+
// Strip inline comments from the request line before parsing
|
|
357
|
+
let requestLine = (0, stringUtils_1.stripInlineComment)(allLines[requestLineIndex].trim());
|
|
358
|
+
// Extract retry/backoff options before parsing method/URL
|
|
359
|
+
const { cleanedLine, retryCount, backoffMs } = extractRetryOptions(requestLine);
|
|
360
|
+
requestLine = cleanedLine;
|
|
361
|
+
const [method, ...urlParts] = requestLine.split(' ');
|
|
362
|
+
let url = urlParts.join(' ');
|
|
363
|
+
// Handle quoted URLs - remove the quotes
|
|
364
|
+
if ((url.startsWith('"') && url.endsWith('"')) || (url.startsWith("'") && url.endsWith("'"))) {
|
|
365
|
+
url = url.slice(1, -1);
|
|
366
|
+
}
|
|
367
|
+
const headers = {};
|
|
368
|
+
let bodyStartIndex = -1;
|
|
369
|
+
let foundBlankLine = false;
|
|
370
|
+
// Parse headers (lines after request line until blank line or non-header)
|
|
371
|
+
// Headers must have format "Name: Value" where Name is a valid token
|
|
372
|
+
const headerRegex = /^([A-Za-z0-9\-_]+)\s*:\s*(.+)$/;
|
|
373
|
+
for (let i = requestLineIndex + 1; i < allLines.length; i++) {
|
|
374
|
+
const line = allLines[i].trim();
|
|
375
|
+
// Skip comment lines and variable declarations in header section
|
|
376
|
+
if (line.startsWith('#') || line.startsWith('var ')) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
// Blank line marks end of headers
|
|
380
|
+
if (line === '') {
|
|
381
|
+
foundBlankLine = true;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
// If we've seen a blank line, everything after is body
|
|
385
|
+
if (foundBlankLine) {
|
|
386
|
+
bodyStartIndex = i;
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
const headerMatch = line.match(headerRegex);
|
|
390
|
+
if (headerMatch) {
|
|
391
|
+
headers[headerMatch[1]] = headerMatch[2].trim();
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
// Not a header format, must be start of body
|
|
395
|
+
bodyStartIndex = i;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Extract body - join remaining non-empty, non-comment lines
|
|
400
|
+
// Stop at the next Norn block boundary so later declarations do not leak into the body.
|
|
401
|
+
let body;
|
|
402
|
+
if (bodyStartIndex > 0) {
|
|
403
|
+
const bodyLines = [];
|
|
404
|
+
for (let i = bodyStartIndex; i < allLines.length; i++) {
|
|
405
|
+
const line = allLines[i].trim();
|
|
406
|
+
// Stop at request boundaries
|
|
407
|
+
if (isRequestLine(line) ||
|
|
408
|
+
isNamedRequestDeclarationLine(line) ||
|
|
409
|
+
isSequenceLine(line) ||
|
|
410
|
+
isEndSequenceLine(line) ||
|
|
411
|
+
line.startsWith('###')) {
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
// Include non-empty, non-comment, non-var lines
|
|
415
|
+
if (line && !line.startsWith('#') && !line.startsWith('var ')) {
|
|
416
|
+
bodyLines.push(line);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
body = bodyLines.join('\n').trim();
|
|
420
|
+
if (body === '') {
|
|
421
|
+
body = undefined;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return { method: method.toUpperCase(), url, headers, body, retryCount, backoffMs };
|
|
425
|
+
}
|
|
426
|
+
const METHOD_REGEX = /^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|TRACE|CONNECT)\s+/i;
|
|
427
|
+
/**
|
|
428
|
+
* Checks if a line is the start of a new request (HTTP method line).
|
|
429
|
+
*/
|
|
430
|
+
function isRequestLine(line) {
|
|
431
|
+
return METHOD_REGEX.test(line.trim());
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Checks if a line is a variable declaration.
|
|
435
|
+
*/
|
|
436
|
+
function isVariableLine(line) {
|
|
437
|
+
return /^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=/.test(line.trim());
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Checks if a line is the start of a sequence block.
|
|
441
|
+
*/
|
|
442
|
+
function isSequenceLine(line) {
|
|
443
|
+
return isSequenceStartDeclaration(line.trim());
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Checks if a line is a named request declaration like [MyRequest].
|
|
447
|
+
*/
|
|
448
|
+
function isNamedRequestDeclarationLine(line) {
|
|
449
|
+
return /^\[[^\]]+\]\s*$/.test(line.trim());
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Checks if a line closes a sequence block.
|
|
453
|
+
*/
|
|
454
|
+
function isEndSequenceLine(line) {
|
|
455
|
+
return isSequenceEndDeclaration(line.trim());
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Finds the request block at a given line number.
|
|
459
|
+
* Request blocks are detected by HTTP method keywords (GET, POST, etc.).
|
|
460
|
+
* No need for ### separators - we detect boundaries automatically.
|
|
461
|
+
*/
|
|
462
|
+
function getRequestBlockAtLine(text, lineNumber) {
|
|
463
|
+
const lines = text.split('\n');
|
|
464
|
+
// First, find the request line at or before the given line number
|
|
465
|
+
let requestStartLine = -1;
|
|
466
|
+
for (let i = lineNumber; i >= 0; i--) {
|
|
467
|
+
if (isRequestLine(lines[i])) {
|
|
468
|
+
requestStartLine = i;
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// If no request line found above, search below (user might be on a comment above the request)
|
|
473
|
+
if (requestStartLine === -1) {
|
|
474
|
+
for (let i = lineNumber; i < lines.length; i++) {
|
|
475
|
+
if (isRequestLine(lines[i])) {
|
|
476
|
+
requestStartLine = i;
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (requestStartLine === -1) {
|
|
482
|
+
return ''; // No request found
|
|
483
|
+
}
|
|
484
|
+
// Find where this request block ends (next request or block declaration)
|
|
485
|
+
let endLine = lines.length;
|
|
486
|
+
for (let i = requestStartLine + 1; i < lines.length; i++) {
|
|
487
|
+
const line = lines[i].trim();
|
|
488
|
+
// End at the next request line, named request, sequence boundary, or ### separator.
|
|
489
|
+
if (isRequestLine(lines[i]) ||
|
|
490
|
+
isNamedRequestDeclarationLine(lines[i]) ||
|
|
491
|
+
isSequenceLine(lines[i]) ||
|
|
492
|
+
isEndSequenceLine(lines[i]) ||
|
|
493
|
+
line.startsWith('###')) {
|
|
494
|
+
endLine = i;
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// Include any comments/blank lines immediately before the request
|
|
499
|
+
let startLine = requestStartLine;
|
|
500
|
+
for (let i = requestStartLine - 1; i >= 0; i--) {
|
|
501
|
+
const line = lines[i].trim();
|
|
502
|
+
if (line === '' || line.startsWith('#')) {
|
|
503
|
+
// Don't include if it's a ### separator
|
|
504
|
+
if (line.startsWith('###')) {
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
startLine = i;
|
|
508
|
+
}
|
|
509
|
+
else if (isVariableLine(lines[i])) {
|
|
510
|
+
// Stop at variable declarations
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
else if (isRequestLine(lines[i])) {
|
|
514
|
+
// Stop at previous request
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return lines.slice(startLine, endLine).join('\n');
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Extracts all import statements from the document.
|
|
525
|
+
* Import syntax:
|
|
526
|
+
* import "path/to/file.norn" (quoted)
|
|
527
|
+
* import './path/to/file.norn' (quoted)
|
|
528
|
+
* import ./path/to/file.norn (unquoted)
|
|
529
|
+
*/
|
|
530
|
+
function extractImports(text) {
|
|
531
|
+
const lines = text.split('\n');
|
|
532
|
+
const imports = [];
|
|
533
|
+
// Match import "path" or import 'path' (quoted)
|
|
534
|
+
const quotedImportRegex = /^\s*import\s+["'](.+?)["']\s*$/;
|
|
535
|
+
// Match import path (unquoted - path without spaces)
|
|
536
|
+
const unquotedImportRegex = /^\s*import\s+(\S+)\s*$/;
|
|
537
|
+
for (let i = 0; i < lines.length; i++) {
|
|
538
|
+
const line = lines[i];
|
|
539
|
+
// Try quoted first
|
|
540
|
+
let match = line.match(quotedImportRegex);
|
|
541
|
+
if (match) {
|
|
542
|
+
imports.push({
|
|
543
|
+
path: match[1],
|
|
544
|
+
lineNumber: i
|
|
545
|
+
});
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
// Try unquoted
|
|
549
|
+
match = line.match(unquotedImportRegex);
|
|
550
|
+
if (match) {
|
|
551
|
+
imports.push({
|
|
552
|
+
path: match[1],
|
|
553
|
+
lineNumber: i
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return imports;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Resolves all imports in a .norn file and returns the combined imported content.
|
|
561
|
+
* Only extracts named requests and sequences from imported files (not variables).
|
|
562
|
+
* Also handles .nornapi imports for header groups and endpoints.
|
|
563
|
+
* Handles circular imports by tracking the active import stack.
|
|
564
|
+
* Re-imports of already loaded files are treated as no-ops.
|
|
565
|
+
*
|
|
566
|
+
* @param text - The source text to extract imports from
|
|
567
|
+
* @param baseDir - The directory of the current file (for resolving relative paths)
|
|
568
|
+
* @param readFile - Function to read file contents (allows async filesystem access)
|
|
569
|
+
* @param alreadyImported - Set of already loaded import paths (for de-duplication)
|
|
570
|
+
* @param importStack - Set of import paths currently being resolved (for true cycle detection)
|
|
571
|
+
*/
|
|
572
|
+
async function resolveImports(text, baseDir, readFile, alreadyImported = new Set(), importStack = new Set(), sourceFilePath = path.resolve(baseDir, '__norn_root__.norn'), sqlImportedPathsBySource = new Map(), sqlOperationsBySource = new Map()) {
|
|
573
|
+
const imports = extractImports(text);
|
|
574
|
+
const errors = [];
|
|
575
|
+
const importedContents = [];
|
|
576
|
+
const resolvedPaths = [];
|
|
577
|
+
const headerGroups = [];
|
|
578
|
+
const endpoints = [];
|
|
579
|
+
// Track sources for duplicate detection
|
|
580
|
+
const headerGroupSources = new Map();
|
|
581
|
+
const endpointSources = new Map();
|
|
582
|
+
const namedRequestSources = new Map();
|
|
583
|
+
const sequenceSources = new Map();
|
|
584
|
+
if (!sqlOperationsBySource.has(sourceFilePath)) {
|
|
585
|
+
sqlOperationsBySource.set(sourceFilePath, new Map());
|
|
586
|
+
}
|
|
587
|
+
if (!sqlImportedPathsBySource.has(sourceFilePath)) {
|
|
588
|
+
sqlImportedPathsBySource.set(sourceFilePath, new Set());
|
|
589
|
+
}
|
|
590
|
+
for (const imp of imports) {
|
|
591
|
+
// Resolve the path relative to baseDir
|
|
592
|
+
const absolutePath = path.resolve(baseDir, imp.path);
|
|
593
|
+
if (imp.path.endsWith('.nornsql')) {
|
|
594
|
+
const importedSqlPaths = sqlImportedPathsBySource.get(sourceFilePath);
|
|
595
|
+
if (importedSqlPaths.has(absolutePath)) {
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
try {
|
|
599
|
+
const content = await readFile(absolutePath);
|
|
600
|
+
importedSqlPaths.add(absolutePath);
|
|
601
|
+
resolvedPaths.push(absolutePath);
|
|
602
|
+
const parsedSql = (0, nornSqlParser_1.parseNornSqlFile)(content, absolutePath);
|
|
603
|
+
for (const parseError of parsedSql.errors) {
|
|
604
|
+
errors.push({
|
|
605
|
+
path: imp.path,
|
|
606
|
+
error: parseError.message,
|
|
607
|
+
lineNumber: parseError.lineNumber >= 0 ? parseError.lineNumber : imp.lineNumber,
|
|
608
|
+
blocking: parseError.blocking
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
const scope = sqlOperationsBySource.get(sourceFilePath);
|
|
612
|
+
for (const operation of parsedSql.operations) {
|
|
613
|
+
const lowerName = operation.name.toLowerCase();
|
|
614
|
+
const existing = scope.get(lowerName);
|
|
615
|
+
if (existing) {
|
|
616
|
+
errors.push({
|
|
617
|
+
path: imp.path,
|
|
618
|
+
error: `Duplicate SQL operation '${operation.name}': already defined in '${existing.sourcePath || existing.connectionName}'`,
|
|
619
|
+
lineNumber: imp.lineNumber,
|
|
620
|
+
blocking: true
|
|
621
|
+
});
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
scope.set(lowerName, operation);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
errors.push({
|
|
629
|
+
path: imp.path,
|
|
630
|
+
error: error.message || 'Failed to read file',
|
|
631
|
+
lineNumber: imp.lineNumber,
|
|
632
|
+
blocking: true
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
// Circular import only when the file is in the active import stack.
|
|
638
|
+
if (importStack.has(absolutePath)) {
|
|
639
|
+
errors.push({
|
|
640
|
+
path: imp.path,
|
|
641
|
+
error: `Circular import detected`,
|
|
642
|
+
lineNumber: imp.lineNumber,
|
|
643
|
+
blocking: true
|
|
644
|
+
});
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
// Duplicate import of an already loaded file: skip silently.
|
|
648
|
+
if (alreadyImported.has(absolutePath)) {
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
try {
|
|
652
|
+
const content = await readFile(absolutePath);
|
|
653
|
+
alreadyImported.add(absolutePath);
|
|
654
|
+
importStack.add(absolutePath);
|
|
655
|
+
resolvedPaths.push(absolutePath);
|
|
656
|
+
// Check if this is a .nornapi file
|
|
657
|
+
if (imp.path.endsWith('.nornapi')) {
|
|
658
|
+
// Parse the .nornapi file
|
|
659
|
+
const apiDef = (0, nornapiParser_1.parseNornApiFile)(content);
|
|
660
|
+
// Check for duplicate header groups
|
|
661
|
+
for (const group of apiDef.headerGroups) {
|
|
662
|
+
const existingSource = headerGroupSources.get(group.name);
|
|
663
|
+
if (existingSource) {
|
|
664
|
+
errors.push({
|
|
665
|
+
path: imp.path,
|
|
666
|
+
error: `Duplicate header group '${group.name}': already defined in '${existingSource}'`,
|
|
667
|
+
lineNumber: imp.lineNumber
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
headerGroupSources.set(group.name, imp.path);
|
|
672
|
+
headerGroups.push(group);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
// Check for duplicate endpoints
|
|
676
|
+
for (const endpoint of apiDef.endpoints) {
|
|
677
|
+
const existingSource = endpointSources.get(endpoint.name);
|
|
678
|
+
if (existingSource) {
|
|
679
|
+
errors.push({
|
|
680
|
+
path: imp.path,
|
|
681
|
+
error: `Duplicate endpoint '${endpoint.name}': already defined in '${existingSource}'`,
|
|
682
|
+
lineNumber: imp.lineNumber
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
endpointSources.set(endpoint.name, imp.path);
|
|
687
|
+
endpoints.push(endpoint);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
// Regular .norn file - process as before
|
|
693
|
+
// Recursively resolve imports in the imported file
|
|
694
|
+
const importDir = path.dirname(absolutePath);
|
|
695
|
+
const nestedResult = await resolveImports(content, importDir, readFile, alreadyImported, importStack, absolutePath, sqlImportedPathsBySource, sqlOperationsBySource);
|
|
696
|
+
// Add nested errors
|
|
697
|
+
errors.push(...nestedResult.errors);
|
|
698
|
+
resolvedPaths.push(...nestedResult.resolvedPaths);
|
|
699
|
+
for (const [nestedName, nestedSource] of nestedResult.sequenceSources.entries()) {
|
|
700
|
+
if (!sequenceSources.has(nestedName)) {
|
|
701
|
+
sequenceSources.set(nestedName, nestedSource);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
// Add nested API definitions with duplicate checking
|
|
705
|
+
for (const group of nestedResult.headerGroups) {
|
|
706
|
+
const existingSource = headerGroupSources.get(group.name);
|
|
707
|
+
if (existingSource) {
|
|
708
|
+
errors.push({
|
|
709
|
+
path: imp.path,
|
|
710
|
+
error: `Duplicate header group '${group.name}': already defined in '${existingSource}'`,
|
|
711
|
+
lineNumber: imp.lineNumber
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
headerGroupSources.set(group.name, imp.path);
|
|
716
|
+
headerGroups.push(group);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
for (const endpoint of nestedResult.endpoints) {
|
|
720
|
+
const existingSource = endpointSources.get(endpoint.name);
|
|
721
|
+
if (existingSource) {
|
|
722
|
+
errors.push({
|
|
723
|
+
path: imp.path,
|
|
724
|
+
error: `Duplicate endpoint '${endpoint.name}': already defined in '${existingSource}'`,
|
|
725
|
+
lineNumber: imp.lineNumber
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
endpointSources.set(endpoint.name, imp.path);
|
|
730
|
+
endpoints.push(endpoint);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
// Add nested imported content first (dependencies first)
|
|
734
|
+
if (nestedResult.importedContent) {
|
|
735
|
+
importedContents.push(nestedResult.importedContent);
|
|
736
|
+
}
|
|
737
|
+
// Extract only file-level variables to resolve references in imported requests/sequences.
|
|
738
|
+
// Sequence-local vars must stay runtime-evaluated and must not be pre-baked.
|
|
739
|
+
const importedVariables = extractFileLevelVariables(content);
|
|
740
|
+
const importedNamedRequests = extractNamedRequests(content);
|
|
741
|
+
const importedSequences = extractSequencesFromText(content);
|
|
742
|
+
// Reconstruct named requests with variables already substituted (with duplicate checking)
|
|
743
|
+
for (const req of importedNamedRequests) {
|
|
744
|
+
const lowerName = req.name.toLowerCase();
|
|
745
|
+
const existingSource = namedRequestSources.get(lowerName);
|
|
746
|
+
if (existingSource) {
|
|
747
|
+
errors.push({
|
|
748
|
+
path: imp.path,
|
|
749
|
+
error: `Duplicate named request '${req.name}': already defined in '${existingSource}'`,
|
|
750
|
+
lineNumber: imp.lineNumber
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
namedRequestSources.set(lowerName, imp.path);
|
|
755
|
+
const resolvedContent = substituteVariables(req.content, importedVariables);
|
|
756
|
+
importedContents.push(`[${req.name}]\n${resolvedContent}`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
// Reconstruct sequences with variables already substituted (with duplicate checking)
|
|
760
|
+
for (const seq of importedSequences) {
|
|
761
|
+
const lowerName = seq.name.toLowerCase();
|
|
762
|
+
const existingSource = sequenceSources.get(lowerName);
|
|
763
|
+
if (existingSource) {
|
|
764
|
+
errors.push({
|
|
765
|
+
path: imp.path,
|
|
766
|
+
error: `Duplicate sequence '${seq.name}': already defined in '${existingSource}'`,
|
|
767
|
+
lineNumber: imp.lineNumber
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
// Store the ABSOLUTE path so script paths can be resolved correctly
|
|
772
|
+
sequenceSources.set(lowerName, absolutePath);
|
|
773
|
+
const resolvedDeclaration = substituteVariables(seq.declaration, importedVariables);
|
|
774
|
+
const resolvedContent = substituteVariables(seq.content, importedVariables);
|
|
775
|
+
importedContents.push(`${resolvedDeclaration}\n${resolvedContent}\nend sequence`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
catch (error) {
|
|
780
|
+
errors.push({
|
|
781
|
+
path: imp.path,
|
|
782
|
+
error: error.message || 'Failed to read file',
|
|
783
|
+
lineNumber: imp.lineNumber
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
finally {
|
|
787
|
+
importStack.delete(absolutePath);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return {
|
|
791
|
+
importedContent: importedContents.join('\n\n'),
|
|
792
|
+
errors,
|
|
793
|
+
resolvedPaths,
|
|
794
|
+
headerGroups,
|
|
795
|
+
endpoints,
|
|
796
|
+
sequenceSources,
|
|
797
|
+
sqlOperationsBySource
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Helper function to extract sequences from text without importing sequenceRunner
|
|
802
|
+
* (avoids circular dependencies)
|
|
803
|
+
*/
|
|
804
|
+
function extractSequencesFromText(text) {
|
|
805
|
+
const lines = text.split('\n');
|
|
806
|
+
const sequences = [];
|
|
807
|
+
let currentSequence = null;
|
|
808
|
+
for (let i = 0; i < lines.length; i++) {
|
|
809
|
+
const line = lines[i].trim();
|
|
810
|
+
const lineWithoutComment = (0, stringUtils_1.stripInlineComment)(line);
|
|
811
|
+
// Check for sequence start. Preserve the parameter list so imported
|
|
812
|
+
// sequences can still bind arguments when reconstructed.
|
|
813
|
+
const sequenceMatch = lineWithoutComment.match(/^sequence\s+([a-zA-Z_][a-zA-Z0-9_-]*)(\s*\([^)]*\))?\s*$/);
|
|
814
|
+
if (sequenceMatch) {
|
|
815
|
+
currentSequence = {
|
|
816
|
+
name: sequenceMatch[1],
|
|
817
|
+
declaration: `sequence ${sequenceMatch[1]}${sequenceMatch[2] || ''}`,
|
|
818
|
+
lines: []
|
|
819
|
+
};
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
// Check for sequence end
|
|
823
|
+
if (lineWithoutComment === 'end sequence' && currentSequence) {
|
|
824
|
+
sequences.push({
|
|
825
|
+
name: currentSequence.name,
|
|
826
|
+
declaration: currentSequence.declaration,
|
|
827
|
+
content: currentSequence.lines.join('\n')
|
|
828
|
+
});
|
|
829
|
+
currentSequence = null;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
// Add line to current sequence
|
|
833
|
+
if (currentSequence) {
|
|
834
|
+
currentSequence.lines.push(lines[i]);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return sequences;
|
|
838
|
+
}
|
|
839
|
+
//# sourceMappingURL=parser.js.map
|