@examplary/cli 1.9.0 → 1.10.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/README.md +10 -0
- package/dist/commands/upload.js +17 -84
- package/dist/commands/validate.d.ts +2 -0
- package/dist/commands/validate.js +23 -0
- package/dist/index.js +2 -0
- package/dist/lib/read-meta.d.ts +5 -9
- package/dist/lib/response-processing.d.ts +14 -0
- package/dist/lib/response-processing.js +45 -0
- package/dist/lib/validate.d.ts +32 -0
- package/dist/lib/validate.js +126 -0
- package/dist/preview-ui/assets/{icon-nodes-Ds7bJID4.js → icon-nodes-DWBwwVXR.js} +1 -1
- package/dist/preview-ui/assets/{icon-nodes-Ds7bJID4.js.map → icon-nodes-DWBwwVXR.js.map} +1 -1
- package/dist/preview-ui/assets/index-e8E6mno-.js +3632 -0
- package/dist/preview-ui/assets/index-e8E6mno-.js.map +1 -0
- package/dist/preview-ui/assets/index-nhKepHkH.css +1 -0
- package/dist/preview-ui/assets/{tags-DZBm0mOh.js → tags-DkzE0dXT.js} +1 -1
- package/dist/preview-ui/assets/{tags-DZBm0mOh.js.map → tags-DkzE0dXT.js.map} +1 -1
- package/dist/preview-ui/index.html +2 -2
- package/package.json +11 -9
- package/dist/preview-ui/assets/index-DcEcr1Zm.css +0 -1
- package/dist/preview-ui/assets/index-NlxMWEJQ.js +0 -3629
- package/dist/preview-ui/assets/index-NlxMWEJQ.js.map +0 -1
package/README.md
CHANGED
|
@@ -24,3 +24,13 @@ npx @examplary/cli upload
|
|
|
24
24
|
You'll be required to pass a `--key` argument with your API key, which you can
|
|
25
25
|
obtain from your Examplary account settings. You can also set the `EXAMPLARY_API_KEY`
|
|
26
26
|
environment variable to avoid passing the key every time.
|
|
27
|
+
|
|
28
|
+
## Validating
|
|
29
|
+
|
|
30
|
+
To run all of the pre-upload checks without uploading anything, use:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npx @examplary/cli validate
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This verifies required fields, JSONata expressions (including `responseProcessing.condition`), response processing test cases, that components bundle, and the icon file. The same checks run automatically as part of `upload`.
|
package/dist/commands/upload.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
|
-
import { readFile } from "fs/promises";
|
|
3
|
-
import { resolve } from "path";
|
|
4
2
|
import { cwd, exit } from "process";
|
|
5
3
|
import { setApiHost, setApiKey, uploadFile } from "../lib/api.js";
|
|
6
|
-
import { buildComponent } from "../lib/bundle.js";
|
|
7
|
-
import { deepCompileJsonata } from "../lib/jsonata.js";
|
|
8
4
|
import { readMetaFile } from "../lib/read-meta.js";
|
|
9
|
-
import {
|
|
5
|
+
import { isValid, printValidationResult, validateDefinition, } from "../lib/validate.js";
|
|
10
6
|
export const uploadCommand = async (argv) => {
|
|
11
7
|
setApiHost(argv.host);
|
|
12
8
|
setApiKey(argv.key);
|
|
@@ -20,98 +16,35 @@ export const uploadCommand = async (argv) => {
|
|
|
20
16
|
exit(1);
|
|
21
17
|
}
|
|
22
18
|
console.log(`Uploading question type: ${definition.id}...`);
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
// Run all checks (incl. response processing test cases) before uploading
|
|
20
|
+
const result = await validateDefinition(definition, dir);
|
|
21
|
+
printValidationResult(result);
|
|
22
|
+
if (!isValid(result)) {
|
|
23
|
+
console.error(`\n🚫 Validation failed, aborting upload`);
|
|
25
24
|
exit(1);
|
|
26
25
|
}
|
|
27
|
-
|
|
28
|
-
console.error(`🚫 Missing required field: name`);
|
|
29
|
-
exit(1);
|
|
30
|
-
}
|
|
31
|
-
// Validate JSONata expressions in the definition
|
|
32
|
-
if (definition.export?.qti?.interaction?.options) {
|
|
33
|
-
try {
|
|
34
|
-
await deepCompileJsonata(definition.export.qti.interaction.options);
|
|
35
|
-
}
|
|
36
|
-
catch (e) {
|
|
37
|
-
console.error(`🚫 Invalid JSONata expression in export.qti.interaction.options: ${e.message}`);
|
|
38
|
-
exit(1);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
// Bundle and upload components
|
|
26
|
+
// Upload the bundled components produced during validation
|
|
42
27
|
const components = {};
|
|
43
|
-
const
|
|
44
|
-
"settings-area",
|
|
45
|
-
"assessment",
|
|
46
|
-
"print",
|
|
47
|
-
"results",
|
|
48
|
-
];
|
|
49
|
-
for (const [componentType, componentPath] of Object.entries(definition.components || {})) {
|
|
50
|
-
// Validation
|
|
51
|
-
if (!validComponentTypes.includes(componentType)) {
|
|
52
|
-
console.error(`🚫 Invalid component type specified: ${componentType}`);
|
|
53
|
-
exit(1);
|
|
54
|
-
}
|
|
55
|
-
const realPath = resolve(dir, componentPath);
|
|
56
|
-
if (!realPath) {
|
|
57
|
-
console.error(`🚫 Missing required file: ${componentPath}`);
|
|
58
|
-
exit(1);
|
|
59
|
-
}
|
|
60
|
-
// Bundle component
|
|
61
|
-
let componentOutput = { js: "" };
|
|
62
|
-
try {
|
|
63
|
-
componentOutput = await buildComponent(realPath);
|
|
64
|
-
const sizeInKb = componentOutput.js.length / 1000;
|
|
65
|
-
const sizeHint = `(${sizeInKb.toFixed(2)} kB)`;
|
|
66
|
-
console.log(`• Bundled component ${componentType} from ${componentPath} ${sizeHint}`);
|
|
67
|
-
if (sizeInKb > 100) {
|
|
68
|
-
console.warn(` ⚠️ Component ${componentType} is larger than 100 kB! Consider optimizing it.`);
|
|
69
|
-
if (process.env.GITHUB_ACTIONS) {
|
|
70
|
-
console.log(`::warning file=${realPath},title=Large component size::Component ${componentType} is larger than 100 kB ${sizeHint}! Consider optimizing it.`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
catch (error) {
|
|
75
|
-
console.error(`🚫 Error bundling component ${componentType} from ${componentPath}: ${error.message}`);
|
|
76
|
-
exit(1);
|
|
77
|
-
}
|
|
28
|
+
for (const [componentType, output] of Object.entries(result.bundles)) {
|
|
78
29
|
// Upload source map first if it exists
|
|
79
30
|
let sourceMapStatement = "";
|
|
80
|
-
if (
|
|
81
|
-
const url = await uploadFile(`component-${componentType}.js.map`,
|
|
31
|
+
if (output.sourcemap) {
|
|
32
|
+
const url = await uploadFile(`component-${componentType}.js.map`, output.sourcemap, "application/json");
|
|
82
33
|
sourceMapStatement = `\n//# sourceMappingURL=${url}`;
|
|
83
34
|
}
|
|
84
|
-
|
|
85
|
-
components[componentType] = await uploadFile(`component-${componentType}.js`, componentOutput.js + sourceMapStatement, "application/javascript");
|
|
35
|
+
components[componentType] = await uploadFile(`component-${componentType}.js`, output.js + sourceMapStatement, "application/javascript");
|
|
86
36
|
console.log(`• Uploaded component ${componentType}`);
|
|
87
37
|
}
|
|
88
38
|
definition.components = components;
|
|
89
39
|
// Upload styles if present
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const sizeInKb = styles.length / 1000;
|
|
95
|
-
const sizeHint = `(${sizeInKb.toFixed(2)} kB)`;
|
|
96
|
-
console.log(`• Uploaded styles ${sizeHint}`);
|
|
97
|
-
}
|
|
40
|
+
if (result.styles) {
|
|
41
|
+
definition.stylesheet = await uploadFile("styles.css", result.styles, "text/css");
|
|
42
|
+
const sizeInKb = (result.styles.length / 1000).toFixed(2);
|
|
43
|
+
console.log(`• Uploaded styles (${sizeInKb} kB)`);
|
|
98
44
|
}
|
|
99
45
|
// Upload icon if specified
|
|
100
|
-
if (definition.icon) {
|
|
101
|
-
|
|
102
|
-
let iconContents;
|
|
103
|
-
try {
|
|
104
|
-
iconContents = await readFile(iconPath, "utf-8");
|
|
105
|
-
}
|
|
106
|
-
catch (error) {
|
|
107
|
-
console.error(`🚫 Error reading icon file: ${error.message}`);
|
|
108
|
-
exit(1);
|
|
109
|
-
}
|
|
110
|
-
if (!iconContents) {
|
|
111
|
-
console.error(`🚫 Icon file is empty: ${iconPath}`);
|
|
112
|
-
exit(1);
|
|
113
|
-
}
|
|
114
|
-
definition.icon = await uploadFile("icon.svg", iconContents, "image/svg+xml");
|
|
46
|
+
if (definition.icon && result.iconContents) {
|
|
47
|
+
definition.icon = await uploadFile("icon.svg", result.iconContents, "image/svg+xml");
|
|
115
48
|
console.log(`• Uploaded icon`);
|
|
116
49
|
}
|
|
117
50
|
// Upload the main question type definition
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { cwd, exit } from "process";
|
|
3
|
+
import { readMetaFile } from "../lib/read-meta.js";
|
|
4
|
+
import { isValid, printValidationResult, validateDefinition, } from "../lib/validate.js";
|
|
5
|
+
export const validateCommand = async () => {
|
|
6
|
+
const dir = cwd();
|
|
7
|
+
let definition = {};
|
|
8
|
+
try {
|
|
9
|
+
({ definition } = await readMetaFile());
|
|
10
|
+
}
|
|
11
|
+
catch (e) {
|
|
12
|
+
console.error(`🚫 ${e.message}`);
|
|
13
|
+
exit(1);
|
|
14
|
+
}
|
|
15
|
+
console.log(`Validating question type: ${definition.id}...`);
|
|
16
|
+
const result = await validateDefinition(definition, dir);
|
|
17
|
+
printValidationResult(result);
|
|
18
|
+
if (!isValid(result)) {
|
|
19
|
+
console.error(`\n🚫 Validation failed`);
|
|
20
|
+
exit(1);
|
|
21
|
+
}
|
|
22
|
+
console.log(`\n✅ Validation passed`);
|
|
23
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import yargs from "yargs";
|
|
|
3
3
|
import { hideBin } from "yargs/helpers";
|
|
4
4
|
import { previewCommand } from "./commands/preview.js";
|
|
5
5
|
import { uploadCommand } from "./commands/upload.js";
|
|
6
|
+
import { validateCommand } from "./commands/validate.js";
|
|
6
7
|
yargs()
|
|
7
8
|
.usage("examplary <cmd> [args]")
|
|
8
9
|
.command("upload", "Bundle and upload a question type definition", (yargs) => {
|
|
@@ -18,6 +19,7 @@ yargs()
|
|
|
18
19
|
default: "https://api.examplary.ai",
|
|
19
20
|
});
|
|
20
21
|
}, (args) => uploadCommand({ host: args.host, key: args.key }))
|
|
22
|
+
.command("validate", "Validate a question type definition without uploading", (yargs) => yargs, () => validateCommand())
|
|
21
23
|
.command("preview", "Run local development server", (yargs) => {
|
|
22
24
|
return yargs
|
|
23
25
|
.option("key", {
|
package/dist/lib/read-meta.d.ts
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
|
+
import type { QuestionType } from "@examplary/schemas";
|
|
2
|
+
export type QuestionTypeDefinition = Partial<QuestionType> & {
|
|
3
|
+
$schema?: string;
|
|
4
|
+
};
|
|
1
5
|
/**
|
|
2
6
|
* Reads and parses the question-type definition file.
|
|
3
7
|
* Prefers question-type.json over question-type.yml.
|
|
4
8
|
*/
|
|
5
9
|
export declare const readMetaFile: () => Promise<{
|
|
6
|
-
definition:
|
|
7
|
-
$schema?: string;
|
|
8
|
-
id?: string;
|
|
9
|
-
name?: string;
|
|
10
|
-
components?: Record<string, string>;
|
|
11
|
-
export?: any;
|
|
12
|
-
stylesheet?: string;
|
|
13
|
-
icon?: string;
|
|
14
|
-
};
|
|
10
|
+
definition: QuestionTypeDefinition;
|
|
15
11
|
path: string;
|
|
16
12
|
}>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { QuestionTypeResponseProcessingType } from "@examplary/schemas";
|
|
2
|
+
export type TestCaseResult = {
|
|
3
|
+
index: number;
|
|
4
|
+
expected: boolean;
|
|
5
|
+
actual?: boolean;
|
|
6
|
+
passed: boolean;
|
|
7
|
+
error?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Runs every `responseProcessing.testCases` through the `condition` and checks
|
|
11
|
+
* that the evaluated outcome matches the `expectedOutcome`. Returns one result
|
|
12
|
+
* per test case; an empty array is returned when there are no test cases.
|
|
13
|
+
*/
|
|
14
|
+
export declare const runResponseProcessingTestCases: (responseProcessing: QuestionTypeResponseProcessingType) => Promise<TestCaseResult[]>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import jsonata from "jsonata";
|
|
2
|
+
/**
|
|
3
|
+
* Evaluates the response processing `condition` against a single test case's
|
|
4
|
+
* `answer` and `question`, mirroring how the API grades responses
|
|
5
|
+
* (see api/src/lib/grading/process-response.ts).
|
|
6
|
+
*/
|
|
7
|
+
const evaluateCondition = async (condition, data) => {
|
|
8
|
+
if (!condition.startsWith("=")) {
|
|
9
|
+
throw new Error("Invalid response processing condition: must start with '='");
|
|
10
|
+
}
|
|
11
|
+
const outcome = await jsonata(condition.substring(1)).evaluate(data);
|
|
12
|
+
return Boolean(outcome);
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Runs every `responseProcessing.testCases` through the `condition` and checks
|
|
16
|
+
* that the evaluated outcome matches the `expectedOutcome`. Returns one result
|
|
17
|
+
* per test case; an empty array is returned when there are no test cases.
|
|
18
|
+
*/
|
|
19
|
+
export const runResponseProcessingTestCases = async (responseProcessing) => {
|
|
20
|
+
const { condition, testCases } = responseProcessing;
|
|
21
|
+
if (!testCases?.length)
|
|
22
|
+
return [];
|
|
23
|
+
const results = [];
|
|
24
|
+
for (let index = 0; index < testCases.length; index++) {
|
|
25
|
+
const { answer, question, expectedOutcome } = testCases[index];
|
|
26
|
+
try {
|
|
27
|
+
const actual = await evaluateCondition(condition, { answer, question });
|
|
28
|
+
results.push({
|
|
29
|
+
index,
|
|
30
|
+
expected: expectedOutcome,
|
|
31
|
+
actual,
|
|
32
|
+
passed: actual === expectedOutcome,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
results.push({
|
|
37
|
+
index,
|
|
38
|
+
expected: expectedOutcome,
|
|
39
|
+
passed: false,
|
|
40
|
+
error: error.message,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return results;
|
|
45
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { QuestionTypeDefinition } from "./read-meta.js";
|
|
2
|
+
import { TestCaseResult } from "./response-processing.js";
|
|
3
|
+
export type BuiltComponent = {
|
|
4
|
+
js: string;
|
|
5
|
+
sourcemap?: string;
|
|
6
|
+
};
|
|
7
|
+
export type ValidationWarning = {
|
|
8
|
+
message: string;
|
|
9
|
+
file?: string;
|
|
10
|
+
};
|
|
11
|
+
export type ValidationResult = {
|
|
12
|
+
errors: string[];
|
|
13
|
+
warnings: ValidationWarning[];
|
|
14
|
+
testCaseResults: TestCaseResult[];
|
|
15
|
+
/** Bundled component output, keyed by component type, ready to upload. */
|
|
16
|
+
bundles: Record<string, BuiltComponent>;
|
|
17
|
+
/** Generated stylesheet, present only when components are bundled. */
|
|
18
|
+
styles?: string;
|
|
19
|
+
/** Raw icon file contents, present only when an icon is configured. */
|
|
20
|
+
iconContents?: string;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Runs every check the `upload` command performs, short of actually uploading:
|
|
24
|
+
* required fields, JSONata syntax, component bundling, icon presence, and the
|
|
25
|
+
* response processing test cases. Bundled artifacts are returned so callers can
|
|
26
|
+
* reuse them for an upload without bundling twice.
|
|
27
|
+
*/
|
|
28
|
+
export declare const validateDefinition: (definition: QuestionTypeDefinition, dir: string) => Promise<ValidationResult>;
|
|
29
|
+
/** Whether the definition is safe to upload. */
|
|
30
|
+
export declare const isValid: (result: ValidationResult) => boolean;
|
|
31
|
+
/** Prints warnings, errors, and a test-case summary to the console. */
|
|
32
|
+
export declare const printValidationResult: (result: ValidationResult) => void;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
import { buildComponent } from "./bundle.js";
|
|
4
|
+
import { deepCompileJsonata } from "./jsonata.js";
|
|
5
|
+
import { runResponseProcessingTestCases, } from "./response-processing.js";
|
|
6
|
+
import { buildStyles } from "./styles.js";
|
|
7
|
+
const VALID_COMPONENT_TYPES = [
|
|
8
|
+
"settings-area",
|
|
9
|
+
"assessment",
|
|
10
|
+
"print",
|
|
11
|
+
"results",
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Runs every check the `upload` command performs, short of actually uploading:
|
|
15
|
+
* required fields, JSONata syntax, component bundling, icon presence, and the
|
|
16
|
+
* response processing test cases. Bundled artifacts are returned so callers can
|
|
17
|
+
* reuse them for an upload without bundling twice.
|
|
18
|
+
*/
|
|
19
|
+
export const validateDefinition = async (definition, dir) => {
|
|
20
|
+
const errors = [];
|
|
21
|
+
const warnings = [];
|
|
22
|
+
const bundles = {};
|
|
23
|
+
let styles;
|
|
24
|
+
let iconContents;
|
|
25
|
+
if (!definition.id)
|
|
26
|
+
errors.push("Missing required field: id");
|
|
27
|
+
if (!definition.name)
|
|
28
|
+
errors.push("Missing required field: name");
|
|
29
|
+
// Validate JSONata expressions in the export options
|
|
30
|
+
if (definition.export?.qti3?.interaction?.options) {
|
|
31
|
+
try {
|
|
32
|
+
await deepCompileJsonata(definition.export.qti3.interaction.options);
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
errors.push(`Invalid JSONata expression in export.qti3.interaction.options: ${e.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Validate the response processing condition and run its test cases
|
|
39
|
+
let testCaseResults = [];
|
|
40
|
+
if (definition.responseProcessing) {
|
|
41
|
+
const condition = definition.responseProcessing.condition;
|
|
42
|
+
if (typeof condition !== "string" || !condition.startsWith("=")) {
|
|
43
|
+
errors.push("Invalid response processing condition: must start with '='");
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
try {
|
|
47
|
+
await deepCompileJsonata(condition);
|
|
48
|
+
testCaseResults = await runResponseProcessingTestCases(definition.responseProcessing);
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
errors.push(`Invalid JSONata expression in responseProcessing.condition: ${e.message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Bundle components to confirm they build and to capture their output
|
|
56
|
+
for (const [componentType, componentPath] of Object.entries(definition.components || {})) {
|
|
57
|
+
if (!VALID_COMPONENT_TYPES.includes(componentType)) {
|
|
58
|
+
errors.push(`Invalid component type specified: ${componentType}`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const realPath = resolve(dir, componentPath);
|
|
62
|
+
try {
|
|
63
|
+
const output = await buildComponent(realPath);
|
|
64
|
+
bundles[componentType] = output;
|
|
65
|
+
const sizeInKb = output.js.length / 1000;
|
|
66
|
+
if (sizeInKb > 100) {
|
|
67
|
+
warnings.push({
|
|
68
|
+
message: `Component ${componentType} is larger than 100 kB (${sizeInKb.toFixed(2)} kB)! Consider optimizing it.`,
|
|
69
|
+
file: realPath,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
errors.push(`Error bundling component ${componentType} from ${componentPath}: ${error.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Build styles if any components are present
|
|
78
|
+
if (Object.keys(bundles).length) {
|
|
79
|
+
styles = (await buildStyles()) ?? undefined;
|
|
80
|
+
}
|
|
81
|
+
// Validate the icon file
|
|
82
|
+
if (definition.icon) {
|
|
83
|
+
const iconPath = resolve(dir, definition.icon);
|
|
84
|
+
try {
|
|
85
|
+
iconContents = await readFile(iconPath, "utf-8");
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
errors.push(`Error reading icon file: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
if (iconContents !== undefined && !iconContents) {
|
|
91
|
+
errors.push(`Icon file is empty: ${iconPath}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { errors, warnings, testCaseResults, bundles, styles, iconContents };
|
|
95
|
+
};
|
|
96
|
+
/** Whether the definition is safe to upload. */
|
|
97
|
+
export const isValid = (result) => result.errors.length === 0 && result.testCaseResults.every((r) => r.passed);
|
|
98
|
+
/** Prints warnings, errors, and a test-case summary to the console. */
|
|
99
|
+
export const printValidationResult = (result) => {
|
|
100
|
+
for (const [componentType, output] of Object.entries(result.bundles)) {
|
|
101
|
+
const sizeInKb = (output.js.length / 1000).toFixed(2);
|
|
102
|
+
console.log(`• Bundled component ${componentType} (${sizeInKb} kB)`);
|
|
103
|
+
}
|
|
104
|
+
for (const warning of result.warnings) {
|
|
105
|
+
console.warn(` ⚠️ ${warning.message}`);
|
|
106
|
+
if (warning.file && process.env.GITHUB_ACTIONS) {
|
|
107
|
+
console.log(`::warning file=${warning.file},title=Validation warning::${warning.message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (result.testCaseResults.length) {
|
|
111
|
+
const passed = result.testCaseResults.filter((r) => r.passed).length;
|
|
112
|
+
const total = result.testCaseResults.length;
|
|
113
|
+
console.log(`• Response processing test cases: ${passed}/${total} passed`);
|
|
114
|
+
for (const r of result.testCaseResults) {
|
|
115
|
+
if (r.passed)
|
|
116
|
+
continue;
|
|
117
|
+
const detail = r.error
|
|
118
|
+
? `errored: ${r.error}`
|
|
119
|
+
: `expected outcome ${r.expected} but got ${r.actual}`;
|
|
120
|
+
console.error(` 🚫 Test case #${r.index + 1} failed (${detail})`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const error of result.errors) {
|
|
124
|
+
console.error(`🚫 ${error}`);
|
|
125
|
+
}
|
|
126
|
+
};
|