norn-cli 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +66 -2
- package/dist/cli.js +192 -23
- package/package.json +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,43 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the "Norn" extension will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.1.3] - 2026-02-01
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **@data shorthand for single parameter**: `@data(1, 2, 3)` with a single parameter now correctly creates 3 test cases instead of 1 case with 3 values
|
|
9
|
+
- Example: `@data(1, 2, 3) test sequence Foo(id)` now runs 3 times with `id=1`, `id=2`, `id=3`
|
|
10
|
+
- Multi-parameter syntax unchanged: `@data(1, "a")` per line for each test case
|
|
11
|
+
|
|
12
|
+
## [1.1.2] - 2026-02-01
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- **VS Code Test Explorer Integration**: Run test sequences from the Testing sidebar
|
|
16
|
+
- Automatic test discovery for all `.norn` files
|
|
17
|
+
- Tests grouped by tag (`@smoke`, `@regression`, etc.)
|
|
18
|
+
- Colorful streaming output with ANSI colors
|
|
19
|
+
- Persistent output when selecting tests
|
|
20
|
+
- Detailed failure info with expected vs actual diffs
|
|
21
|
+
- Request/response details for failed HTTP calls
|
|
22
|
+
|
|
23
|
+
- **Parameterized Tests**: Data-driven testing with `@data` and `@theory` annotations
|
|
24
|
+
- `@data(1, "Widget")` syntax for inline test data
|
|
25
|
+
- `@theory("./testdata.json")` for external data files
|
|
26
|
+
- Typed value parsing (numbers, booleans, strings)
|
|
27
|
+
- Each data row creates a separate test case in Test Explorer
|
|
28
|
+
- Example: `@data(1, 200)` with `test sequence ValidateStatus(id, expectedStatus)`
|
|
29
|
+
|
|
30
|
+
- **Test Sequence Validation**: Diagnostics for test annotations
|
|
31
|
+
- Error if `@data`/`@theory` used on regular sequences (must use `test sequence`)
|
|
32
|
+
- Error if tags like `@smoke` used on regular sequences
|
|
33
|
+
- Error if test sequence has required params but no `@data`/`@theory`
|
|
34
|
+
|
|
35
|
+
### Improved
|
|
36
|
+
- **Test Explorer Output**: Rich colorized output with icons
|
|
37
|
+
- HTTP methods in cyan, status codes color-coded (green/red/yellow)
|
|
38
|
+
- Checkmarks for passed assertions, X for failures
|
|
39
|
+
- Clear test headers when running multiple tests
|
|
40
|
+
- Duration shown for each test
|
|
41
|
+
|
|
5
42
|
## [1.1.0] - 2026-02-01
|
|
6
43
|
|
|
7
44
|
### Added
|
package/README.md
CHANGED
|
@@ -13,6 +13,8 @@ A powerful REST client extension for VS Code with sequences, assertions, environ
|
|
|
13
13
|
- **Environments**: Manage dev/staging/prod configurations with `.nornenv` files
|
|
14
14
|
- **Sequences**: Chain multiple requests with response capture using `$N.path`
|
|
15
15
|
- **Test Sequences**: Mark sequences as tests with `test sequence` for CLI execution
|
|
16
|
+
- **Test Explorer**: Run tests from VS Code's Testing sidebar with colorful output
|
|
17
|
+
- **Parameterized Tests**: Data-driven testing with `@data` and `@theory` annotations
|
|
16
18
|
- **Sequence Tags**: Tag sequences with `@smoke`, `@team(CustomerExp)` for filtering in CI/CD
|
|
17
19
|
- **Secret Variables**: Mark sensitive environment variables with `secret` for automatic redaction
|
|
18
20
|
- **Assertions**: Validate responses with `assert` statements supporting comparison, type checking, and existence
|
|
@@ -753,6 +755,65 @@ npx norn --help
|
|
|
753
755
|
| `--no-fail` | Don't exit with error code on failed tests |
|
|
754
756
|
| `-h, --help` | Show help message |
|
|
755
757
|
|
|
758
|
+
## Test Explorer
|
|
759
|
+
|
|
760
|
+
Run tests directly from VS Code's Testing sidebar:
|
|
761
|
+
|
|
762
|
+
- **Automatic Discovery**: Test sequences appear in the Testing view
|
|
763
|
+
- **Tag Grouping**: Tests organized by tags (`@smoke`, `@regression`, etc.)
|
|
764
|
+
- **Colorful Output**: ANSI-colored results with icons and status codes
|
|
765
|
+
- **Persistent Output**: Select a test to see its full output anytime
|
|
766
|
+
- **Failure Details**: Expected vs actual diffs, request/response info
|
|
767
|
+
|
|
768
|
+
### Parameterized Tests
|
|
769
|
+
|
|
770
|
+
Use `@data` for data-driven testing - each data row becomes a separate test:
|
|
771
|
+
|
|
772
|
+
```bash
|
|
773
|
+
# Single parameter - runs 3 times with id = 1, 2, 3
|
|
774
|
+
@data(1, 2, 3)
|
|
775
|
+
test sequence TodoTest(id)
|
|
776
|
+
GET {{baseUrl}}/todos/{{id}}
|
|
777
|
+
assert $1.status == 200
|
|
778
|
+
assert $1.body.id == {{id}}
|
|
779
|
+
end sequence
|
|
780
|
+
|
|
781
|
+
# Multiple parameters - runs 2 times
|
|
782
|
+
@data(1, "delectus aut autem")
|
|
783
|
+
@data(2, "quis ut nam facilis")
|
|
784
|
+
test sequence TodoTitleTest(id, expectedTitle)
|
|
785
|
+
GET {{baseUrl}}/todos/{{id}}
|
|
786
|
+
assert $1.status == 200
|
|
787
|
+
assert $1.body.title == "{{expectedTitle}}"
|
|
788
|
+
end sequence
|
|
789
|
+
|
|
790
|
+
# Typed values (numbers, booleans, strings)
|
|
791
|
+
@data(1, true, "active")
|
|
792
|
+
@data(2, false, "inactive")
|
|
793
|
+
test sequence UserStatusTest(userId, isActive, status)
|
|
794
|
+
GET {{baseUrl}}/users/{{userId}}
|
|
795
|
+
assert $1.status == 200
|
|
796
|
+
end sequence
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
Use `@theory` for external data files:
|
|
800
|
+
|
|
801
|
+
```bash
|
|
802
|
+
@theory("./testdata.json")
|
|
803
|
+
test sequence DataFileTest(id, name)
|
|
804
|
+
GET {{baseUrl}}/items/{{id}}
|
|
805
|
+
assert $1.body.name == "{{name}}"
|
|
806
|
+
end sequence
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
Where `testdata.json` contains:
|
|
810
|
+
```json
|
|
811
|
+
[
|
|
812
|
+
{"id": 1, "name": "Widget"},
|
|
813
|
+
{"id": 2, "name": "Gadget"}
|
|
814
|
+
]
|
|
815
|
+
```
|
|
816
|
+
|
|
756
817
|
### CI/CD Example (GitHub Actions)
|
|
757
818
|
|
|
758
819
|
```yaml
|
|
@@ -793,8 +854,11 @@ jobs:
|
|
|
793
854
|
| `run RequestName` | Execute a named request |
|
|
794
855
|
| `sequence Name` | Start a helper sequence block |
|
|
795
856
|
| `test sequence Name` | Start a test sequence (runs from CLI) |
|
|
796
|
-
|
|
|
797
|
-
| `@
|
|
857
|
+
| `test sequence Name(params)` | Test sequence with parameters |
|
|
858
|
+
| `@tagname` | Simple tag on a test sequence |
|
|
859
|
+
| `@key(value)` | Key-value tag on a test sequence |
|
|
860
|
+
| `@data(val1, val2)` | Inline test data for parameterized tests |
|
|
861
|
+
| `@theory("file.json")` | External test data file |
|
|
798
862
|
| `end sequence` | End a sequence block |
|
|
799
863
|
| `var x = $1.path` | Capture value from response 1 |
|
|
800
864
|
| `$N.status` | Access status code of response N |
|
package/dist/cli.js
CHANGED
|
@@ -20098,6 +20098,8 @@ function parseSequenceParameters(line2) {
|
|
|
20098
20098
|
}
|
|
20099
20099
|
function parseSequenceTags(lines, sequenceLineIndex) {
|
|
20100
20100
|
const tags = [];
|
|
20101
|
+
const dataCases = [];
|
|
20102
|
+
let theorySource;
|
|
20101
20103
|
let tagStartLine = sequenceLineIndex;
|
|
20102
20104
|
for (let i = sequenceLineIndex - 1; i >= 0; i--) {
|
|
20103
20105
|
const line2 = lines[i].trim();
|
|
@@ -20107,10 +20109,26 @@ function parseSequenceTags(lines, sequenceLineIndex) {
|
|
|
20107
20109
|
if (!line2.startsWith("@")) {
|
|
20108
20110
|
break;
|
|
20109
20111
|
}
|
|
20112
|
+
const dataMatch = line2.match(/^@data\s*\((.+)\)\s*$/);
|
|
20113
|
+
if (dataMatch) {
|
|
20114
|
+
const values = parseDataValues(dataMatch[1]);
|
|
20115
|
+
dataCases.push(values);
|
|
20116
|
+
tagStartLine = i;
|
|
20117
|
+
continue;
|
|
20118
|
+
}
|
|
20119
|
+
const theoryMatch = line2.match(/^@theory\s*\(\s*["']([^"']+)["']\s*\)\s*$/);
|
|
20120
|
+
if (theoryMatch) {
|
|
20121
|
+
theorySource = theoryMatch[1];
|
|
20122
|
+
tagStartLine = i;
|
|
20123
|
+
continue;
|
|
20124
|
+
}
|
|
20110
20125
|
const tagPattern = /@([a-zA-Z_][a-zA-Z0-9_-]*)(?:\(([^)]+)\))?/g;
|
|
20111
20126
|
let match;
|
|
20112
20127
|
while ((match = tagPattern.exec(line2)) !== null) {
|
|
20113
20128
|
const tagName = match[1];
|
|
20129
|
+
if (tagName === "data" || tagName === "theory") {
|
|
20130
|
+
continue;
|
|
20131
|
+
}
|
|
20114
20132
|
const tagValue = match[2]?.trim();
|
|
20115
20133
|
tags.push({
|
|
20116
20134
|
name: tagName,
|
|
@@ -20119,7 +20137,75 @@ function parseSequenceTags(lines, sequenceLineIndex) {
|
|
|
20119
20137
|
}
|
|
20120
20138
|
tagStartLine = i;
|
|
20121
20139
|
}
|
|
20122
|
-
|
|
20140
|
+
let theoryData;
|
|
20141
|
+
if (dataCases.length > 0 || theorySource) {
|
|
20142
|
+
theoryData = {
|
|
20143
|
+
cases: [],
|
|
20144
|
+
// Will be populated with param names after we know them
|
|
20145
|
+
source: theorySource
|
|
20146
|
+
};
|
|
20147
|
+
theoryData._rawCases = dataCases.reverse();
|
|
20148
|
+
}
|
|
20149
|
+
return { tags, tagStartLine, theoryData };
|
|
20150
|
+
}
|
|
20151
|
+
function parseDataValues(valuesStr) {
|
|
20152
|
+
const values = [];
|
|
20153
|
+
let current = "";
|
|
20154
|
+
let inQuote = null;
|
|
20155
|
+
let i = 0;
|
|
20156
|
+
while (i < valuesStr.length) {
|
|
20157
|
+
const char = valuesStr[i];
|
|
20158
|
+
if (inQuote) {
|
|
20159
|
+
if (char === inQuote) {
|
|
20160
|
+
values.push(current);
|
|
20161
|
+
current = "";
|
|
20162
|
+
inQuote = null;
|
|
20163
|
+
i++;
|
|
20164
|
+
while (i < valuesStr.length && valuesStr[i] !== ",") {
|
|
20165
|
+
i++;
|
|
20166
|
+
}
|
|
20167
|
+
i++;
|
|
20168
|
+
continue;
|
|
20169
|
+
} else {
|
|
20170
|
+
current += char;
|
|
20171
|
+
}
|
|
20172
|
+
} else {
|
|
20173
|
+
if (char === '"' || char === "'") {
|
|
20174
|
+
inQuote = char;
|
|
20175
|
+
current = "";
|
|
20176
|
+
} else if (char === ",") {
|
|
20177
|
+
const trimmed = current.trim();
|
|
20178
|
+
if (trimmed) {
|
|
20179
|
+
values.push(parseTypedValue(trimmed));
|
|
20180
|
+
}
|
|
20181
|
+
current = "";
|
|
20182
|
+
} else {
|
|
20183
|
+
current += char;
|
|
20184
|
+
}
|
|
20185
|
+
}
|
|
20186
|
+
i++;
|
|
20187
|
+
}
|
|
20188
|
+
if (inQuote) {
|
|
20189
|
+
values.push(current);
|
|
20190
|
+
} else {
|
|
20191
|
+
const trimmed = current.trim();
|
|
20192
|
+
if (trimmed) {
|
|
20193
|
+
values.push(parseTypedValue(trimmed));
|
|
20194
|
+
}
|
|
20195
|
+
}
|
|
20196
|
+
return values;
|
|
20197
|
+
}
|
|
20198
|
+
function parseTypedValue(value) {
|
|
20199
|
+
if (value === "true") return true;
|
|
20200
|
+
if (value === "false") return false;
|
|
20201
|
+
if (value === "null") return null;
|
|
20202
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
20203
|
+
return parseFloat(value);
|
|
20204
|
+
}
|
|
20205
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
20206
|
+
return value.slice(1, -1);
|
|
20207
|
+
}
|
|
20208
|
+
return value;
|
|
20123
20209
|
}
|
|
20124
20210
|
function extractSequences(text) {
|
|
20125
20211
|
const lines = text.split("\n");
|
|
@@ -20133,7 +20219,29 @@ function extractSequences(text) {
|
|
|
20133
20219
|
const isTest = !!testSequenceMatch;
|
|
20134
20220
|
const name = isTest ? testSequenceMatch[1] : sequenceMatch[1];
|
|
20135
20221
|
const parameters = parseSequenceParameters(lines[i]);
|
|
20136
|
-
const { tags, tagStartLine } = parseSequenceTags(lines, i);
|
|
20222
|
+
const { tags, tagStartLine, theoryData } = parseSequenceTags(lines, i);
|
|
20223
|
+
let finalTheoryData = theoryData;
|
|
20224
|
+
if (theoryData && theoryData._rawCases) {
|
|
20225
|
+
const rawCases = theoryData._rawCases;
|
|
20226
|
+
if (parameters.length === 1 && rawCases.length === 1 && rawCases[0].length > 1) {
|
|
20227
|
+
const paramName = parameters[0].name;
|
|
20228
|
+
finalTheoryData = {
|
|
20229
|
+
cases: rawCases[0].map((value) => ({ [paramName]: value })),
|
|
20230
|
+
source: theoryData.source
|
|
20231
|
+
};
|
|
20232
|
+
} else {
|
|
20233
|
+
finalTheoryData = {
|
|
20234
|
+
cases: rawCases.map((values) => {
|
|
20235
|
+
const caseObj = {};
|
|
20236
|
+
parameters.forEach((param, idx) => {
|
|
20237
|
+
caseObj[param.name] = values[idx];
|
|
20238
|
+
});
|
|
20239
|
+
return caseObj;
|
|
20240
|
+
}),
|
|
20241
|
+
source: theoryData.source
|
|
20242
|
+
};
|
|
20243
|
+
}
|
|
20244
|
+
}
|
|
20137
20245
|
currentSequence = {
|
|
20138
20246
|
name,
|
|
20139
20247
|
startLine: tagStartLine,
|
|
@@ -20141,7 +20249,8 @@ function extractSequences(text) {
|
|
|
20141
20249
|
lines: [],
|
|
20142
20250
|
parameters,
|
|
20143
20251
|
tags,
|
|
20144
|
-
isTest
|
|
20252
|
+
isTest,
|
|
20253
|
+
theoryData: finalTheoryData
|
|
20145
20254
|
};
|
|
20146
20255
|
continue;
|
|
20147
20256
|
}
|
|
@@ -20153,7 +20262,8 @@ function extractSequences(text) {
|
|
|
20153
20262
|
content: currentSequence.lines.join("\n"),
|
|
20154
20263
|
parameters: currentSequence.parameters,
|
|
20155
20264
|
tags: currentSequence.tags,
|
|
20156
|
-
isTest: currentSequence.isTest
|
|
20265
|
+
isTest: currentSequence.isTest,
|
|
20266
|
+
theoryData: currentSequence.theoryData
|
|
20157
20267
|
});
|
|
20158
20268
|
currentSequence = null;
|
|
20159
20269
|
continue;
|
|
@@ -23024,34 +23134,93 @@ function countTestSequences(fileContent, tagFilterOptions) {
|
|
|
23024
23134
|
const filtered = tagFilterOptions && tagFilterOptions.filters.length > 0 ? testSequences.filter((seq) => sequenceMatchesTags(seq, tagFilterOptions)).length : testSequences.length;
|
|
23025
23135
|
return { total: testSequences.length, filtered };
|
|
23026
23136
|
}
|
|
23137
|
+
async function loadTheoryFile(theoryPath, workingDir) {
|
|
23138
|
+
const absolutePath = path3.resolve(workingDir, theoryPath);
|
|
23139
|
+
const content = await fsPromises.readFile(absolutePath, "utf-8");
|
|
23140
|
+
return JSON.parse(content);
|
|
23141
|
+
}
|
|
23142
|
+
function formatCaseLabel(params) {
|
|
23143
|
+
const parts = Object.entries(params).map(([key, value]) => {
|
|
23144
|
+
if (typeof value === "string") {
|
|
23145
|
+
return `${key}="${value}"`;
|
|
23146
|
+
}
|
|
23147
|
+
return `${key}=${value}`;
|
|
23148
|
+
});
|
|
23149
|
+
return `[${parts.join(", ")}]`;
|
|
23150
|
+
}
|
|
23027
23151
|
async function runAllSequences(fileContent, variables, cookieJar, workingDir, apiDefinitions, tagFilterOptions) {
|
|
23028
23152
|
const allSequences = extractSequences(fileContent);
|
|
23029
23153
|
const results = [];
|
|
23030
23154
|
const testSequences = allSequences.filter((seq) => seq.isTest);
|
|
23031
23155
|
const sequences = tagFilterOptions && tagFilterOptions.filters.length > 0 ? testSequences.filter((seq) => sequenceMatchesTags(seq, tagFilterOptions)) : testSequences;
|
|
23032
23156
|
for (const seq of sequences) {
|
|
23033
|
-
|
|
23034
|
-
if (
|
|
23035
|
-
|
|
23036
|
-
|
|
23037
|
-
|
|
23157
|
+
let theoryData = seq.theoryData;
|
|
23158
|
+
if (theoryData?.source && theoryData.cases.length === 0) {
|
|
23159
|
+
try {
|
|
23160
|
+
const cases = await loadTheoryFile(theoryData.source, workingDir);
|
|
23161
|
+
theoryData = { ...theoryData, cases };
|
|
23162
|
+
} catch (error) {
|
|
23163
|
+
const errorResult = {
|
|
23164
|
+
name: seq.name,
|
|
23165
|
+
success: false,
|
|
23166
|
+
responses: [],
|
|
23167
|
+
scriptResults: [],
|
|
23168
|
+
assertionResults: [],
|
|
23169
|
+
steps: [],
|
|
23170
|
+
errors: [`Failed to load theory file '${theoryData.source}': ${error instanceof Error ? error.message : String(error)}`],
|
|
23171
|
+
duration: 0
|
|
23172
|
+
};
|
|
23173
|
+
results.push(errorResult);
|
|
23174
|
+
continue;
|
|
23175
|
+
}
|
|
23176
|
+
}
|
|
23177
|
+
if (theoryData && theoryData.cases.length > 0) {
|
|
23178
|
+
for (let caseIdx = 0; caseIdx < theoryData.cases.length; caseIdx++) {
|
|
23179
|
+
const caseParams = theoryData.cases[caseIdx];
|
|
23180
|
+
const caseLabel = formatCaseLabel(caseParams);
|
|
23181
|
+
const caseArgs = {};
|
|
23182
|
+
for (const [key, value] of Object.entries(caseParams)) {
|
|
23183
|
+
caseArgs[key] = String(value);
|
|
23184
|
+
}
|
|
23185
|
+
const result = await runSequenceWithJar(
|
|
23186
|
+
seq.content,
|
|
23187
|
+
variables,
|
|
23188
|
+
cookieJar,
|
|
23189
|
+
workingDir,
|
|
23190
|
+
fileContent,
|
|
23191
|
+
void 0,
|
|
23192
|
+
void 0,
|
|
23193
|
+
caseArgs,
|
|
23194
|
+
apiDefinitions,
|
|
23195
|
+
tagFilterOptions
|
|
23196
|
+
);
|
|
23197
|
+
result.name = `${seq.name}${caseLabel}`;
|
|
23198
|
+
results.push(result);
|
|
23199
|
+
}
|
|
23200
|
+
} else {
|
|
23201
|
+
const defaultArgs = {};
|
|
23202
|
+
if (seq.parameters) {
|
|
23203
|
+
for (const param of seq.parameters) {
|
|
23204
|
+
if (param.defaultValue !== void 0) {
|
|
23205
|
+
defaultArgs[param.name] = param.defaultValue;
|
|
23206
|
+
}
|
|
23038
23207
|
}
|
|
23039
23208
|
}
|
|
23209
|
+
const result = await runSequenceWithJar(
|
|
23210
|
+
seq.content,
|
|
23211
|
+
variables,
|
|
23212
|
+
cookieJar,
|
|
23213
|
+
workingDir,
|
|
23214
|
+
fileContent,
|
|
23215
|
+
void 0,
|
|
23216
|
+
void 0,
|
|
23217
|
+
defaultArgs,
|
|
23218
|
+
apiDefinitions,
|
|
23219
|
+
tagFilterOptions
|
|
23220
|
+
);
|
|
23221
|
+
result.name = seq.name;
|
|
23222
|
+
results.push(result);
|
|
23040
23223
|
}
|
|
23041
|
-
const result = await runSequenceWithJar(
|
|
23042
|
-
seq.content,
|
|
23043
|
-
variables,
|
|
23044
|
-
cookieJar,
|
|
23045
|
-
workingDir,
|
|
23046
|
-
fileContent,
|
|
23047
|
-
void 0,
|
|
23048
|
-
void 0,
|
|
23049
|
-
defaultArgs,
|
|
23050
|
-
apiDefinitions,
|
|
23051
|
-
tagFilterOptions
|
|
23052
|
-
);
|
|
23053
|
-
result.name = seq.name;
|
|
23054
|
-
results.push(result);
|
|
23055
23224
|
}
|
|
23056
23225
|
return results;
|
|
23057
23226
|
}
|
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.1.
|
|
5
|
+
"version": "1.1.3",
|
|
6
6
|
"publisher": "Norn-PeterKrustanov",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Peter Krastanov"
|
|
@@ -168,7 +168,10 @@
|
|
|
168
168
|
"check-types": "tsc --noEmit",
|
|
169
169
|
"lint": "eslint src",
|
|
170
170
|
"test": "vscode-test",
|
|
171
|
-
"test:regression": "./tests/Regression/run-all.sh"
|
|
171
|
+
"test:regression": "./tests/Regression/run-all.sh",
|
|
172
|
+
"publish:npm": "node -e \"const p=require('./package.json');p.name='norn-cli';require('fs').writeFileSync('package.json',JSON.stringify(p,null,2))\" && npm publish && node -e \"const p=require('./package.json');p.name='norn';require('fs').writeFileSync('package.json',JSON.stringify(p,null,2))\"",
|
|
173
|
+
"publish:vsce": "node -e \"const p=require('./package.json');p.name='norn';require('fs').writeFileSync('package.json',JSON.stringify(p,null,2))\" && npx vsce publish",
|
|
174
|
+
"publish:all": "npm run publish:npm && npm run publish:vsce"
|
|
172
175
|
},
|
|
173
176
|
"devDependencies": {
|
|
174
177
|
"@types/mocha": "^10.0.10",
|
|
@@ -191,4 +194,4 @@
|
|
|
191
194
|
"bin": {
|
|
192
195
|
"norn": "./dist/cli.js"
|
|
193
196
|
}
|
|
194
|
-
}
|
|
197
|
+
}
|