norn-cli 1.6.2 → 1.7.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/cli.js +135 -37
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@ All notable changes to the "Norn" extension will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.7.0] - 2026-03-07
8
+
9
+ ### Added
10
+ - **Explicit Environment Lookup (Extension + CLI)**:
11
+ - Added `{{$env.name}}` so tests, sequences, endpoints, header groups, and interpolated strings can read directly from the active environment even when a file variable, local variable, or sequence parameter uses the same name.
12
+ - Added IntelliSense for the `$env` namespace and environment variable suggestions after typing `{{$env.`.
13
+ - Added diagnostics for invalid `{{$env...}}` references and warnings when local/file/parameter variables shadow environment variables.
14
+
15
+ ### Fixed
16
+ - **Assertion String Literal Escaping**:
17
+ - Fixed escaped quotes in assertion string literals and quoted `var` expressions so values like `\"{orderId}\"` compare correctly instead of failing with visually identical expected/actual output.
18
+
19
+ - **Parameterized Test Snippet UX**:
20
+ - Updated `@data` completion to insert `data()` with the cursor inside the parentheses instead of placeholder values.
21
+
22
+ - **Decorator Syntax Highlighting**:
23
+ - Fixed `.norn` syntax coloring so decorator lines such as `@data(...)` keep their highlighting when they follow request blocks like `DELETE ...`.
24
+
7
25
  ## [1.6.2] - 2026-03-07
8
26
 
9
27
  ### Fixed
package/dist/cli.js CHANGED
@@ -18783,6 +18783,39 @@ var init_schemaGenerator = __esm({
18783
18783
  }
18784
18784
  });
18785
18785
 
18786
+ // src/quotedString.ts
18787
+ function decodeQuotedStringLiteral(literal) {
18788
+ if (literal.length < 2) {
18789
+ return literal;
18790
+ }
18791
+ const quoteChar = literal[0];
18792
+ if (quoteChar !== '"' && quoteChar !== "'" || literal[literal.length - 1] !== quoteChar) {
18793
+ return literal;
18794
+ }
18795
+ const inner = literal.slice(1, -1);
18796
+ let decoded = "";
18797
+ for (let i = 0; i < inner.length; i++) {
18798
+ const char = inner[i];
18799
+ if (char !== "\\" || i === inner.length - 1) {
18800
+ decoded += char;
18801
+ continue;
18802
+ }
18803
+ const nextChar = inner[i + 1];
18804
+ if (nextChar === "\\" || nextChar === quoteChar) {
18805
+ decoded += nextChar;
18806
+ i++;
18807
+ continue;
18808
+ }
18809
+ decoded += char;
18810
+ }
18811
+ return decoded;
18812
+ }
18813
+ var init_quotedString = __esm({
18814
+ "src/quotedString.ts"() {
18815
+ "use strict";
18816
+ }
18817
+ });
18818
+
18786
18819
  // src/assertionRunner.ts
18787
18820
  var assertionRunner_exports = {};
18788
18821
  __export(assertionRunner_exports, {
@@ -18806,7 +18839,7 @@ function parseAssertCommand(line2) {
18806
18839
  if (pipeIndex !== -1) {
18807
18840
  message = content.substring(pipeIndex + 1).trim();
18808
18841
  if (message.startsWith('"') && message.endsWith('"') || message.startsWith("'") && message.endsWith("'")) {
18809
- message = message.slice(1, -1);
18842
+ message = decodeQuotedStringLiteral(message);
18810
18843
  }
18811
18844
  content = content.substring(0, pipeIndex).trim();
18812
18845
  }
@@ -18848,8 +18881,17 @@ function parseAssertCommand(line2) {
18848
18881
  function findUnquotedPipe(str) {
18849
18882
  let inQuote = false;
18850
18883
  let quoteChar = "";
18884
+ let escapeNext = false;
18851
18885
  for (let i = 0; i < str.length; i++) {
18852
18886
  const char = str[i];
18887
+ if (escapeNext) {
18888
+ escapeNext = false;
18889
+ continue;
18890
+ }
18891
+ if (inQuote && char === "\\") {
18892
+ escapeNext = true;
18893
+ continue;
18894
+ }
18853
18895
  if ((char === '"' || char === "'") && !inQuote) {
18854
18896
  inQuote = true;
18855
18897
  quoteChar = char;
@@ -18934,11 +18976,28 @@ function resolveValue(expr, responses, variables, getValueByPath2, responseIndex
18934
18976
  }
18935
18977
  }
18936
18978
  }
18937
- const varMatch = trimmed.match(/^\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}$/);
18979
+ const varMatch = trimmed.match(/^\{\{(\$env|[a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])*)\}\}$/);
18938
18980
  if (varMatch) {
18939
18981
  const varName = varMatch[1];
18982
+ const pathPart = varMatch[2] || "";
18940
18983
  if (varName in variables) {
18941
18984
  const varValue = variables[varName];
18985
+ if (pathPart) {
18986
+ if (typeof varValue === "object" && varValue !== null) {
18987
+ const path10 = pathPart.replace(/^\./, "");
18988
+ return { value: getNestedValue2(varValue, path10) };
18989
+ }
18990
+ if (typeof varValue === "string") {
18991
+ try {
18992
+ const parsed = JSON.parse(varValue);
18993
+ const path10 = pathPart.replace(/^\./, "");
18994
+ return { value: getNestedValue2(parsed, path10) };
18995
+ } catch {
18996
+ return { value: void 0, error: `Cannot access path on non-object variable: ${varName}` };
18997
+ }
18998
+ }
18999
+ return { value: void 0, error: `Cannot access path on non-object variable: ${varName}` };
19000
+ }
18942
19001
  if (typeof varValue === "string" && /^-?\d+(\.\d+)?$/.test(varValue)) {
18943
19002
  return { value: parseFloat(varValue) };
18944
19003
  }
@@ -18947,7 +19006,7 @@ function resolveValue(expr, responses, variables, getValueByPath2, responseIndex
18947
19006
  return { value: void 0, error: `Variable {{${varName}}} is not defined` };
18948
19007
  }
18949
19008
  if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
18950
- return { value: trimmed.slice(1, -1) };
19009
+ return { value: decodeQuotedStringLiteral(trimmed) };
18951
19010
  }
18952
19011
  if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
18953
19012
  return { value: parseFloat(trimmed) };
@@ -19227,6 +19286,7 @@ var init_assertionRunner = __esm({
19227
19286
  "src/assertionRunner.ts"() {
19228
19287
  "use strict";
19229
19288
  init_schemaGenerator();
19289
+ init_quotedString();
19230
19290
  }
19231
19291
  });
19232
19292
 
@@ -19850,9 +19910,7 @@ function getEndpoint(definition, name) {
19850
19910
  function resolveHeaderValues(headerGroup, variables) {
19851
19911
  const resolved = {};
19852
19912
  for (const [name, value] of Object.entries(headerGroup.headers)) {
19853
- resolved[name] = value.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (_, varName) => {
19854
- return variables[varName] ?? `{{${varName}}}`;
19855
- });
19913
+ resolved[name] = substituteVariables(value, variables);
19856
19914
  }
19857
19915
  return resolved;
19858
19916
  }
@@ -20149,9 +20207,29 @@ function valueToString(value) {
20149
20207
  }
20150
20208
  return String(value);
20151
20209
  }
20210
+ var ENV_SCOPE_KEY = "$env";
20211
+ function attachEnvironmentScope(variables, envVariables) {
20212
+ if (!envVariables) {
20213
+ return variables;
20214
+ }
20215
+ Object.defineProperty(variables, ENV_SCOPE_KEY, {
20216
+ value: envVariables,
20217
+ enumerable: false,
20218
+ configurable: true,
20219
+ writable: false
20220
+ });
20221
+ return variables;
20222
+ }
20223
+ function copyEnvironmentScope(target, source) {
20224
+ const envScope = source[ENV_SCOPE_KEY];
20225
+ if (envScope && typeof envScope === "object") {
20226
+ attachEnvironmentScope(target, envScope);
20227
+ }
20228
+ return target;
20229
+ }
20152
20230
  function substituteVariables(text, variables) {
20153
20231
  return text.replace(
20154
- /\{\{(\$\d+|[a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_-]*|\[\d+\])*)\}\}/g,
20232
+ /\{\{(\$env|\$\d+|[a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_-]*|\[\d+\])*)\}\}/g,
20155
20233
  (match, varName, pathPart) => {
20156
20234
  if (!(varName in variables)) {
20157
20235
  return match;
@@ -20161,8 +20239,14 @@ function substituteVariables(text, variables) {
20161
20239
  if (pathPart) {
20162
20240
  const path10 = pathPart.replace(/^\./, "");
20163
20241
  const nestedValue = getNestedValue(value, path10);
20242
+ if (nestedValue === void 0 && varName === ENV_SCOPE_KEY) {
20243
+ return match;
20244
+ }
20164
20245
  return valueToString(nestedValue);
20165
20246
  }
20247
+ if (varName === ENV_SCOPE_KEY) {
20248
+ return match;
20249
+ }
20166
20250
  return valueToString(value);
20167
20251
  }
20168
20252
  if (pathPart && typeof value === "string") {
@@ -20170,8 +20254,14 @@ function substituteVariables(text, variables) {
20170
20254
  const parsed = JSON.parse(value);
20171
20255
  const path10 = pathPart.replace(/^\./, "");
20172
20256
  const nestedValue = getNestedValue(parsed, path10);
20257
+ if (nestedValue === void 0 && varName === ENV_SCOPE_KEY) {
20258
+ return match;
20259
+ }
20173
20260
  return valueToString(nestedValue);
20174
20261
  } catch {
20262
+ if (varName === ENV_SCOPE_KEY) {
20263
+ return match;
20264
+ }
20175
20265
  return value;
20176
20266
  }
20177
20267
  }
@@ -26940,7 +27030,7 @@ function formatUserFacingError(error, context) {
26940
27030
  }
26941
27031
 
26942
27032
  // src/requestValidation.ts
26943
- var PLACEHOLDER_REGEX = /\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g;
27033
+ var PLACEHOLDER_REGEX = /\{\{(\$env|\$[0-9]+|[a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_-]*|\[\d+\])*)\}\}/g;
26944
27034
  function collectUnresolvedPlaceholders(text) {
26945
27035
  if (!text) {
26946
27036
  return [];
@@ -26949,15 +27039,23 @@ function collectUnresolvedPlaceholders(text) {
26949
27039
  let match;
26950
27040
  PLACEHOLDER_REGEX.lastIndex = 0;
26951
27041
  while ((match = PLACEHOLDER_REGEX.exec(text)) !== null) {
26952
- names.add(match[1]);
27042
+ names.add(match[0]);
26953
27043
  }
26954
27044
  return [...names];
26955
27045
  }
27046
+ function extractEnvScopedNames(placeholders) {
27047
+ return placeholders.filter((value) => value.startsWith("{{$env.")).map((value) => value.slice("{{$env.".length, -2).split(/[.[\]]/)[0]).filter(Boolean);
27048
+ }
26956
27049
  function buildMissingVariableHint(missingVars, context) {
26957
27050
  const env3 = context?.environment;
27051
+ const envScopedNames = extractEnvScopedNames(missingVars);
27052
+ const unscopedVars = missingVars.filter((value) => !value.startsWith("{{$env."));
26958
27053
  if (env3?.hasEnvFile && env3.availableEnvironments && env3.availableEnvironments.length > 0 && !env3.activeEnvironment) {
26959
27054
  return `A .nornenv file was found but no environment is selected. Select one of: ${env3.availableEnvironments.join(", ")}.`;
26960
27055
  }
27056
+ if (env3?.hasEnvFile && env3.activeEnvironment && envScopedNames.length > 0 && unscopedVars.length === 0) {
27057
+ return `Check that ${envScopedNames.map((v) => `'${v}'`).join(", ")} exist in the active environment '${env3.activeEnvironment}'.`;
27058
+ }
26961
27059
  if (env3?.hasEnvFile && env3.activeEnvironment) {
26962
27060
  return `Check that ${missingVars.map((v) => `'${v}'`).join(", ")} exist in the active environment '${env3.activeEnvironment}' or as file-level variables.`;
26963
27061
  }
@@ -26975,7 +27073,7 @@ function describeUnresolvedLocation(label, vars) {
26975
27073
  if (vars.length === 0) {
26976
27074
  return void 0;
26977
27075
  }
26978
- return `${label}: unresolved ${vars.length === 1 ? "variable" : "variables"} ${vars.map((v) => `{{${v}}}`).join(", ")}`;
27076
+ return `${label}: unresolved ${vars.length === 1 ? "variable" : "variables"} ${vars.join(", ")}`;
26979
27077
  }
26980
27078
  function shouldValidateBodyPlaceholders(parsed) {
26981
27079
  const method = String(parsed.method || "").toUpperCase();
@@ -26995,7 +27093,7 @@ function validatePreparedRequest(parsed, context) {
26995
27093
  throw new NornError({
26996
27094
  category: "variable-resolution",
26997
27095
  code: "unresolved-request-variables",
26998
- message: `Request could not be prepared: unresolved ${missingVars.length === 1 ? "variable" : "variables"} ${missingVars.map((v) => `{{${v}}}`).join(", ")}.`,
27096
+ message: `Request could not be prepared: unresolved ${missingVars.length === 1 ? "variable" : "variables"} ${missingVars.join(", ")}.`,
26999
27097
  details,
27000
27098
  hint: buildMissingVariableHint(missingVars, context),
27001
27099
  context: {
@@ -27029,6 +27127,7 @@ function validatePreparedRequest(parsed, context) {
27029
27127
  }
27030
27128
 
27031
27129
  // src/sequenceRunner.ts
27130
+ init_quotedString();
27032
27131
  init_assertionRunner();
27033
27132
  function applyHeaderGroupsToRequest(parsed, requestText, headerGroups, variables) {
27034
27133
  const lines = requestText.split("\n");
@@ -27191,7 +27290,7 @@ function parseRunArguments(argsStr) {
27191
27290
  if (namedMatch) {
27192
27291
  let value = namedMatch[2].trim();
27193
27292
  if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
27194
- value = value.slice(1, -1);
27293
+ value = decodeQuotedStringLiteral(value);
27195
27294
  }
27196
27295
  args.push({
27197
27296
  name: namedMatch[1],
@@ -27200,7 +27299,7 @@ function parseRunArguments(argsStr) {
27200
27299
  } else {
27201
27300
  let value = trimmed;
27202
27301
  if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
27203
- value = value.slice(1, -1);
27302
+ value = decodeQuotedStringLiteral(value);
27204
27303
  }
27205
27304
  args.push({ value });
27206
27305
  }
@@ -27371,7 +27470,7 @@ function parseSequenceParameters(line2) {
27371
27470
  hasSeenDefault = true;
27372
27471
  let defaultValue = defaultMatch[2].trim();
27373
27472
  if (defaultValue.startsWith('"') && defaultValue.endsWith('"') || defaultValue.startsWith("'") && defaultValue.endsWith("'")) {
27374
- defaultValue = defaultValue.slice(1, -1);
27473
+ defaultValue = decodeQuotedStringLiteral(defaultValue);
27375
27474
  }
27376
27475
  params.push({
27377
27476
  name: defaultMatch[1],
@@ -27498,7 +27597,7 @@ function parseTypedValue(value) {
27498
27597
  return parseFloat(value);
27499
27598
  }
27500
27599
  if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
27501
- return value.slice(1, -1);
27600
+ return decodeQuotedStringLiteral(value);
27502
27601
  }
27503
27602
  return value;
27504
27603
  }
@@ -27624,7 +27723,7 @@ function parseVarAssignCommand(line2) {
27624
27723
  function evaluateValueExpression(expr, runtimeVariables) {
27625
27724
  const trimmed = expr.trim();
27626
27725
  if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
27627
- const inner = trimmed.slice(1, -1);
27726
+ const inner = decodeQuotedStringLiteral(trimmed);
27628
27727
  const substituted = substituteVariables(inner, runtimeVariables);
27629
27728
  return { value: substituted };
27630
27729
  }
@@ -27749,7 +27848,7 @@ function resolveBareVariables(text, variables) {
27749
27848
  const resolvedParts = parts.map((part) => {
27750
27849
  const partTrimmed = part.trim();
27751
27850
  if (partTrimmed.startsWith('"') && partTrimmed.endsWith('"') || partTrimmed.startsWith("'") && partTrimmed.endsWith("'")) {
27752
- return partTrimmed.slice(1, -1);
27851
+ return decodeQuotedStringLiteral(partTrimmed);
27753
27852
  }
27754
27853
  const varMatch = partTrimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])*)$/);
27755
27854
  if (varMatch) {
@@ -27775,8 +27874,19 @@ function splitExpressionParts(expr) {
27775
27874
  let current = "";
27776
27875
  let inString = false;
27777
27876
  let stringChar = "";
27877
+ let escapeNext = false;
27778
27878
  for (let i = 0; i < expr.length; i++) {
27779
27879
  const char = expr[i];
27880
+ if (escapeNext) {
27881
+ current += char;
27882
+ escapeNext = false;
27883
+ continue;
27884
+ }
27885
+ if (inString && char === "\\") {
27886
+ current += char;
27887
+ escapeNext = true;
27888
+ continue;
27889
+ }
27780
27890
  if (!inString && (char === '"' || char === "'")) {
27781
27891
  inString = true;
27782
27892
  stringChar = char;
@@ -28232,7 +28342,7 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
28232
28342
  });
28233
28343
  }
28234
28344
  };
28235
- const runtimeVariables = { ...fileVariables, ...sequenceArgs };
28345
+ const runtimeVariables = copyEnvironmentScope({ ...fileVariables, ...sequenceArgs }, fileVariables);
28236
28346
  const buildRequestValidationContext = (stepNumber, method, url2, requestName) => ({
28237
28347
  source: "sequence",
28238
28348
  filePath: executionContext?.filePath,
@@ -28994,19 +29104,13 @@ ${indentMultiline(userMessage)}`;
28994
29104
  apiRequest.endpointName
28995
29105
  );
28996
29106
  if (endpoint) {
28997
- let resolvedPath = endpoint.path;
28998
- resolvedPath = resolvedPath.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (_, varName) => {
28999
- return runtimeVariables[varName] !== void 0 ? String(runtimeVariables[varName]) : `{{${varName}}}`;
29000
- });
29107
+ let resolvedPath = substituteVariables(endpoint.path, runtimeVariables);
29001
29108
  for (const [paramName, paramValue] of Object.entries(apiRequest.params)) {
29002
29109
  let resolvedValue = paramValue;
29003
29110
  if (runtimeVariables[paramValue] !== void 0) {
29004
29111
  resolvedValue = String(runtimeVariables[paramValue]);
29005
- } else if (paramValue.startsWith("{{") && paramValue.endsWith("}}")) {
29006
- const varName = paramValue.slice(2, -2);
29007
- if (runtimeVariables[varName] !== void 0) {
29008
- resolvedValue = String(runtimeVariables[varName]);
29009
- }
29112
+ } else {
29113
+ resolvedValue = substituteVariables(paramValue, runtimeVariables);
29010
29114
  }
29011
29115
  resolvedPath = resolvedPath.replace(`{${paramName}}`, resolvedValue);
29012
29116
  }
@@ -29019,10 +29123,7 @@ ${indentMultiline(userMessage)}`;
29019
29123
  }
29020
29124
  }
29021
29125
  for (const [headerName, headerValue] of Object.entries(apiRequest.inlineHeaders)) {
29022
- let resolved = headerValue;
29023
- resolved = resolved.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (_, varName) => {
29024
- return runtimeVariables[varName] !== void 0 ? String(runtimeVariables[varName]) : `{{${varName}}}`;
29025
- });
29126
+ const resolved = substituteVariables(headerValue, runtimeVariables);
29026
29127
  combinedHeaders[headerName] = resolved;
29027
29128
  }
29028
29129
  const bodyParsed = parserHttpRequest(namedRequest.content, runtimeVariables);
@@ -32114,10 +32215,7 @@ async function runSingleRequest(fileContent, variables, cookieJar, apiDefinition
32114
32215
  }
32115
32216
  }
32116
32217
  for (const [headerName, headerValue] of Object.entries(apiRequest.inlineHeaders)) {
32117
- let resolved = headerValue;
32118
- resolved = resolved.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (_, varName) => {
32119
- return variables[varName] !== void 0 ? String(variables[varName]) : `{{${varName}}}`;
32120
- });
32218
+ const resolved = substituteVariables(headerValue, variables);
32121
32219
  combinedHeaders[headerName] = resolved;
32122
32220
  }
32123
32221
  const parsed2 = {
@@ -32408,7 +32506,7 @@ async function main() {
32408
32506
  const redaction2 = createRedactionOptions(combinedSecretNames, combinedSecretValues, !options.noRedact);
32409
32507
  const fileContent = fs10.readFileSync(filePath, "utf-8");
32410
32508
  const fileVariables = extractFileLevelVariables(fileContent);
32411
- const variables = { ...resolvedEnv.variables, ...fileVariables };
32509
+ const variables = attachEnvironmentScope({ ...resolvedEnv.variables, ...fileVariables }, resolvedEnv.variables);
32412
32510
  const cookieJar = createCookieJar();
32413
32511
  const workingDir = path9.dirname(filePath);
32414
32512
  const importResult = await resolveImports(
@@ -32624,7 +32722,7 @@ ${fileContent}` : fileContent;
32624
32722
  const redaction2 = createRedactionOptions(combinedSecretNames, combinedSecretValues, !options.noRedact);
32625
32723
  const fileContent = fs10.readFileSync(filePath, "utf-8");
32626
32724
  const fileVariables = extractFileLevelVariables(fileContent);
32627
- const variables = { ...resolvedEnv.variables, ...fileVariables };
32725
+ const variables = attachEnvironmentScope({ ...resolvedEnv.variables, ...fileVariables }, resolvedEnv.variables);
32628
32726
  const cookieJar = createCookieJar();
32629
32727
  const workingDir = path9.dirname(filePath);
32630
32728
  const importResult = await resolveImports(
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "norn-cli",
3
3
  "displayName": "Norn - REST Client",
4
4
  "description": "A powerful REST client for making HTTP requests with sequences, variables, scripts, and cookie support",
5
- "version": "1.6.2",
5
+ "version": "1.7.0",
6
6
  "publisher": "Norn-PeterKrustanov",
7
7
  "author": {
8
8
  "name": "Peter Krastanov"