@examplary/cli 1.6.0 → 1.9.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 +1 -1
- package/dist/commands/preview.d.ts +5 -0
- package/dist/commands/preview.js +174 -0
- package/dist/commands/upload.d.ts +5 -0
- package/dist/commands/upload.js +138 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +37 -0
- package/dist/lib/api.d.ts +5 -0
- package/dist/lib/api.js +47 -0
- package/dist/lib/bundle.d.ts +4 -0
- package/dist/lib/bundle.js +27 -0
- package/dist/lib/jsonata.d.ts +1 -0
- package/dist/lib/jsonata.js +26 -0
- package/dist/lib/read-meta.d.ts +16 -0
- package/dist/lib/read-meta.js +36 -0
- package/dist/lib/styles.d.ts +1 -0
- package/dist/lib/styles.js +24 -0
- package/dist/preview-ui/assets/icon-nodes-Ds7bJID4.js +2 -0
- package/dist/preview-ui/assets/icon-nodes-Ds7bJID4.js.map +1 -0
- package/dist/preview-ui/assets/index-DcEcr1Zm.css +1 -0
- package/dist/preview-ui/assets/index-NlxMWEJQ.js +3629 -0
- package/dist/preview-ui/assets/index-NlxMWEJQ.js.map +1 -0
- package/dist/preview-ui/assets/tags-DZBm0mOh.js +2 -0
- package/dist/preview-ui/assets/tags-DZBm0mOh.js.map +1 -0
- package/{src → dist}/preview-ui/index.html +2 -1
- package/package.json +26 -25
- package/bin/index.js +0 -37
- package/src/commands/preview.js +0 -220
- package/src/commands/upload.js +0 -179
- package/src/lib/api.js +0 -53
- package/src/lib/bundle.js +0 -35
- package/src/lib/jsonata.js +0 -31
- package/src/lib/middleware.js +0 -16
- package/src/lib/read-meta.js +0 -40
- package/src/lib/styles.js +0 -32
- package/src/preview-ui/App.tsx +0 -148
- package/src/preview-ui/components/json-editor.tsx +0 -27
- package/src/preview-ui/components/preview-assessment.tsx +0 -43
- package/src/preview-ui/components/preview-print.tsx +0 -38
- package/src/preview-ui/components/preview-results.tsx +0 -38
- package/src/preview-ui/components/preview-settings-area.tsx +0 -98
- package/src/preview-ui/components/question-component.tsx +0 -75
- package/src/preview-ui/index.css +0 -11
- package/src/preview-ui/lib/demo-api.ts +0 -29
- package/src/preview-ui/lib/i18n.ts +0 -15
- package/src/preview-ui/main.tsx +0 -8
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
This package provides a command-line tool to develop and upload custom question types for
|
|
4
4
|
the Examplary platform.
|
|
5
5
|
|
|
6
|
-
Read more in the [📘 Developer Documentation](https://
|
|
6
|
+
Read more in the [📘 Developer Documentation](https://examplary.ai/developers/question-types).
|
|
7
7
|
|
|
8
8
|
## Usage
|
|
9
9
|
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
import { watch } from "fs";
|
|
3
|
+
import { readFile } from "fs/promises";
|
|
4
|
+
import { createServer as createHttpServer } from "http";
|
|
5
|
+
import { resolve } from "path";
|
|
6
|
+
import { cwd, exit } from "process";
|
|
7
|
+
import { serve } from "@hono/node-server";
|
|
8
|
+
import { serveStatic } from "@hono/node-server/serve-static";
|
|
9
|
+
import { Hono } from "hono";
|
|
10
|
+
import open from "open";
|
|
11
|
+
import { WebSocketServer } from "ws";
|
|
12
|
+
import { setApiHost, setApiKey } from "../lib/api.js";
|
|
13
|
+
import { buildComponent } from "../lib/bundle.js";
|
|
14
|
+
import { readMetaFile } from "../lib/read-meta.js";
|
|
15
|
+
import { buildStyles } from "../lib/styles.js";
|
|
16
|
+
export const previewCommand = async (argv) => {
|
|
17
|
+
setApiHost(argv.host);
|
|
18
|
+
setApiKey(argv.key);
|
|
19
|
+
const dir = cwd();
|
|
20
|
+
// Verify a question-type file exists (json or yml)
|
|
21
|
+
try {
|
|
22
|
+
await readMetaFile();
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
console.error(`🚫 ${e.message}`);
|
|
26
|
+
exit(1);
|
|
27
|
+
}
|
|
28
|
+
// Let's start a Vite server to preview the question type
|
|
29
|
+
const root = resolve(import.meta.dirname, "..", "preview-ui");
|
|
30
|
+
const app = new Hono();
|
|
31
|
+
app.get("/api/bundle", async (c) => {
|
|
32
|
+
const path = c.req.query("path");
|
|
33
|
+
const sourcemap = c.req.query("sourcemap") === "1";
|
|
34
|
+
const componentName = c.req.query("componentName");
|
|
35
|
+
try {
|
|
36
|
+
const componentOutput = await buildComponent(path);
|
|
37
|
+
if (sourcemap) {
|
|
38
|
+
return c.body(componentOutput.sourcemap, 200, {
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const sizeInKb = componentOutput.js.length / 1000;
|
|
44
|
+
if (sizeInKb > 100) {
|
|
45
|
+
console.warn(`⚠️ Component ${componentName} is larger than 100 kB! Consider optimizing it.`);
|
|
46
|
+
}
|
|
47
|
+
let output = componentOutput.js;
|
|
48
|
+
if (componentOutput.sourcemap) {
|
|
49
|
+
output += `\n//# sourceMappingURL=/api/bundle?componentName=${componentName}&path=${encodeURIComponent(path || "")}&sourcemap=1`;
|
|
50
|
+
}
|
|
51
|
+
return c.body(output, 200, {
|
|
52
|
+
"Content-Type": "application/javascript",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error(`🚫 Error bundling component ${componentName} from ${path}: ${error.message}`);
|
|
58
|
+
return c.text(`Error bundling component ${componentName}: ${error.message}`, 500);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
app.get("/api/icon", async (c) => {
|
|
62
|
+
const path = c.req.query("path") || "";
|
|
63
|
+
const icon = await readFile(resolve(dir, path), "utf-8");
|
|
64
|
+
return c.body(icon, 200, {
|
|
65
|
+
"Content-Type": "image/svg+xml",
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
app.get("/api/stylesheet", async (c) => {
|
|
69
|
+
const componentName = c.req.query("componentName");
|
|
70
|
+
try {
|
|
71
|
+
const componentOutput = await buildStyles();
|
|
72
|
+
return c.body(componentOutput || "", 200, {
|
|
73
|
+
"Content-Type": "text/css",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
console.error(`🚫 Error building CSS for component ${componentName}: ${error.message}`);
|
|
78
|
+
return c.text(`Error building CSS for component ${componentName}: ${error.message}`, 500);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
app.get("/__reload", async (c) => {
|
|
82
|
+
if (wss) {
|
|
83
|
+
wss.clients.forEach((client) => {
|
|
84
|
+
if (client.readyState === 1) {
|
|
85
|
+
// WebSocket.OPEN
|
|
86
|
+
client.send(JSON.stringify({ type: "reload" }));
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return c.body(null, 201);
|
|
91
|
+
});
|
|
92
|
+
let wsPort; // WebSocket port will be set when server starts
|
|
93
|
+
app.get("/", async (c) => {
|
|
94
|
+
let questionType;
|
|
95
|
+
try {
|
|
96
|
+
({ definition: questionType } = await readMetaFile());
|
|
97
|
+
questionType.components = questionType.components
|
|
98
|
+
? Object.entries(questionType.components).reduce((acc, [componentName, componentPath]) => ({
|
|
99
|
+
...acc,
|
|
100
|
+
[componentName]: `/api/bundle?componentName=${componentName}&path=${encodeURIComponent(componentPath)}`,
|
|
101
|
+
}), {})
|
|
102
|
+
: questionType.components;
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
return c.text(`Error reading question type definition: ${e.message}`, 500);
|
|
106
|
+
}
|
|
107
|
+
let template = await readFile(resolve(root, "index.html"), "utf-8");
|
|
108
|
+
// Inject hot reload script
|
|
109
|
+
const hotReloadScript = `
|
|
110
|
+
<script>
|
|
111
|
+
(function() {
|
|
112
|
+
const wsPort = ${wsPort || "null"};
|
|
113
|
+
if (wsPort) {
|
|
114
|
+
const ws = new WebSocket('ws://localhost:' + wsPort);
|
|
115
|
+
ws.onmessage = function(event) {
|
|
116
|
+
const data = JSON.parse(event.data);
|
|
117
|
+
if (data.type === 'reload') {
|
|
118
|
+
console.log('🔄 Reloading page due to file change...');
|
|
119
|
+
window.location.reload();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
ws.onopen = function() {
|
|
123
|
+
console.log('🔗 Hot reload connected');
|
|
124
|
+
};
|
|
125
|
+
ws.onclose = function() {
|
|
126
|
+
console.log('🔌 Hot reload disconnected');
|
|
127
|
+
};
|
|
128
|
+
ws.onerror = function(error) {
|
|
129
|
+
console.log('❌ Hot reload error:', error);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
})();
|
|
133
|
+
</script>
|
|
134
|
+
`;
|
|
135
|
+
template =
|
|
136
|
+
"<script>window._questionType = " +
|
|
137
|
+
JSON.stringify(questionType) +
|
|
138
|
+
`;</script>${template}${hotReloadScript}`;
|
|
139
|
+
return c.html(template);
|
|
140
|
+
});
|
|
141
|
+
app.use("/assets/*", serveStatic({ root }));
|
|
142
|
+
// Set up file watcher
|
|
143
|
+
const watchDir = cwd();
|
|
144
|
+
console.log(`📁 Watching ${watchDir} for changes...`);
|
|
145
|
+
let wss; // Will be initialized after server starts
|
|
146
|
+
const watcher = watch(watchDir, { recursive: true }, (filename) => {
|
|
147
|
+
if (filename && !filename.startsWith(".")) {
|
|
148
|
+
// Broadcast reload message to all connected clients
|
|
149
|
+
if (wss) {
|
|
150
|
+
wss.clients.forEach((client) => {
|
|
151
|
+
if (client.readyState === 1) {
|
|
152
|
+
// WebSocket.OPEN
|
|
153
|
+
client.send(JSON.stringify({ type: "reload" }));
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
// Clean up watcher on process exit
|
|
160
|
+
process.on("SIGINT", () => {
|
|
161
|
+
watcher.close();
|
|
162
|
+
process.exit(0);
|
|
163
|
+
});
|
|
164
|
+
serve({ fetch: app.fetch, port: 5777 }, (address) => {
|
|
165
|
+
const port = address.port;
|
|
166
|
+
wsPort = port + 1234;
|
|
167
|
+
const wsServer = createHttpServer();
|
|
168
|
+
wss = new WebSocketServer({ server: wsServer });
|
|
169
|
+
wsServer.listen(wsPort);
|
|
170
|
+
const url = `http://localhost:${port}`;
|
|
171
|
+
console.log(`🚀 Preview server running at ${url}`);
|
|
172
|
+
open(url);
|
|
173
|
+
});
|
|
174
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
import { readFile } from "fs/promises";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { cwd, exit } from "process";
|
|
5
|
+
import { setApiHost, setApiKey, uploadFile } from "../lib/api.js";
|
|
6
|
+
import { buildComponent } from "../lib/bundle.js";
|
|
7
|
+
import { deepCompileJsonata } from "../lib/jsonata.js";
|
|
8
|
+
import { readMetaFile } from "../lib/read-meta.js";
|
|
9
|
+
import { buildStyles } from "../lib/styles.js";
|
|
10
|
+
export const uploadCommand = async (argv) => {
|
|
11
|
+
setApiHost(argv.host);
|
|
12
|
+
setApiKey(argv.key);
|
|
13
|
+
const dir = cwd();
|
|
14
|
+
let definition = {};
|
|
15
|
+
try {
|
|
16
|
+
({ definition } = await readMetaFile());
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
console.error(`🚫 ${e.message}`);
|
|
20
|
+
exit(1);
|
|
21
|
+
}
|
|
22
|
+
console.log(`Uploading question type: ${definition.id}...`);
|
|
23
|
+
if (!definition.id) {
|
|
24
|
+
console.error(`🚫 Missing required field: id`);
|
|
25
|
+
exit(1);
|
|
26
|
+
}
|
|
27
|
+
if (!definition.name) {
|
|
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
|
|
42
|
+
const components = {};
|
|
43
|
+
const validComponentTypes = [
|
|
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
|
+
}
|
|
78
|
+
// Upload source map first if it exists
|
|
79
|
+
let sourceMapStatement = "";
|
|
80
|
+
if (componentOutput.sourcemap) {
|
|
81
|
+
const url = await uploadFile(`component-${componentType}.js.map`, componentOutput.sourcemap, "application/json");
|
|
82
|
+
sourceMapStatement = `\n//# sourceMappingURL=${url}`;
|
|
83
|
+
}
|
|
84
|
+
// Upload component
|
|
85
|
+
components[componentType] = await uploadFile(`component-${componentType}.js`, componentOutput.js + sourceMapStatement, "application/javascript");
|
|
86
|
+
console.log(`• Uploaded component ${componentType}`);
|
|
87
|
+
}
|
|
88
|
+
definition.components = components;
|
|
89
|
+
// Upload styles if present
|
|
90
|
+
if (Object.keys(definition.components).length) {
|
|
91
|
+
const styles = await buildStyles();
|
|
92
|
+
if (styles) {
|
|
93
|
+
definition.stylesheet = await uploadFile("styles.css", styles, "text/css");
|
|
94
|
+
const sizeInKb = styles.length / 1000;
|
|
95
|
+
const sizeHint = `(${sizeInKb.toFixed(2)} kB)`;
|
|
96
|
+
console.log(`• Uploaded styles ${sizeHint}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Upload icon if specified
|
|
100
|
+
if (definition.icon) {
|
|
101
|
+
const iconPath = resolve(dir, definition.icon);
|
|
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");
|
|
115
|
+
console.log(`• Uploaded icon`);
|
|
116
|
+
}
|
|
117
|
+
// Upload the main question type definition
|
|
118
|
+
const res = await fetch(`${argv.host}/question-types`, {
|
|
119
|
+
headers: {
|
|
120
|
+
Authorization: `Bearer ${argv.key}`,
|
|
121
|
+
"Content-Type": "application/json",
|
|
122
|
+
},
|
|
123
|
+
method: "POST",
|
|
124
|
+
body: JSON.stringify(definition),
|
|
125
|
+
});
|
|
126
|
+
if (!res.ok) {
|
|
127
|
+
console.error(`🚫 Uploading question type failed`);
|
|
128
|
+
try {
|
|
129
|
+
const errorResponse = await res.json();
|
|
130
|
+
console.log(errorResponse);
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
console.log(e);
|
|
134
|
+
}
|
|
135
|
+
exit(1);
|
|
136
|
+
}
|
|
137
|
+
console.log(`• Finished uploading definition\n`);
|
|
138
|
+
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#! /usr/bin/env node
|
|
2
|
+
import yargs from "yargs";
|
|
3
|
+
import { hideBin } from "yargs/helpers";
|
|
4
|
+
import { previewCommand } from "./commands/preview.js";
|
|
5
|
+
import { uploadCommand } from "./commands/upload.js";
|
|
6
|
+
yargs()
|
|
7
|
+
.usage("examplary <cmd> [args]")
|
|
8
|
+
.command("upload", "Bundle and upload a question type definition", (yargs) => {
|
|
9
|
+
return yargs
|
|
10
|
+
.option("key", {
|
|
11
|
+
type: "string",
|
|
12
|
+
description: "API key for authentication (uses EXAMPLARY_API_KEY env var if not provided)",
|
|
13
|
+
default: process.env.EXAMPLARY_API_KEY || "",
|
|
14
|
+
})
|
|
15
|
+
.option("host", {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "API host to upload to",
|
|
18
|
+
default: "https://api.examplary.ai",
|
|
19
|
+
});
|
|
20
|
+
}, (args) => uploadCommand({ host: args.host, key: args.key }))
|
|
21
|
+
.command("preview", "Run local development server", (yargs) => {
|
|
22
|
+
return yargs
|
|
23
|
+
.option("key", {
|
|
24
|
+
type: "string",
|
|
25
|
+
description: "API key for authentication (uses EXAMPLARY_API_KEY env var if not provided)",
|
|
26
|
+
default: process.env.EXAMPLARY_API_KEY || "",
|
|
27
|
+
})
|
|
28
|
+
.option("host", {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "API host to upload to",
|
|
31
|
+
default: "https://api.examplary.ai",
|
|
32
|
+
});
|
|
33
|
+
}, (args) => previewCommand({ host: args.host, key: args.key }))
|
|
34
|
+
.help()
|
|
35
|
+
.recommendCommands()
|
|
36
|
+
.demandCommand()
|
|
37
|
+
.parse(hideBin(process.argv));
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare let API_HOST: string;
|
|
2
|
+
export declare let API_KEY: string;
|
|
3
|
+
export declare const setApiKey: (key: string) => void;
|
|
4
|
+
export declare const setApiHost: (host: string) => void;
|
|
5
|
+
export declare const uploadFile: (fileName: string, fileContents: any, contentType: string) => Promise<any>;
|
package/dist/lib/api.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { basename } from "path";
|
|
2
|
+
import { exit } from "process";
|
|
3
|
+
// These get overwritten by the CLI options
|
|
4
|
+
export let API_HOST = "https://api.examplary.ai";
|
|
5
|
+
export let API_KEY = process.env.EXAMPLARY_API_KEY || "";
|
|
6
|
+
export const setApiKey = (key) => {
|
|
7
|
+
if (!key)
|
|
8
|
+
return;
|
|
9
|
+
API_KEY = key;
|
|
10
|
+
};
|
|
11
|
+
export const setApiHost = (host) => {
|
|
12
|
+
if (!host)
|
|
13
|
+
return;
|
|
14
|
+
API_HOST = host;
|
|
15
|
+
};
|
|
16
|
+
export const uploadFile = async (fileName, fileContents, contentType) => {
|
|
17
|
+
const params = new URLSearchParams();
|
|
18
|
+
params.append("filename", basename(fileName));
|
|
19
|
+
params.append("contentType", contentType);
|
|
20
|
+
params.append("type", "question-type");
|
|
21
|
+
// Request the upload URL
|
|
22
|
+
const res = await fetch(`${API_HOST}/media/upload?${params.toString()}`, {
|
|
23
|
+
headers: {
|
|
24
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
const errorText = await res.text();
|
|
29
|
+
console.error(`🚫 Upload failed: ${errorText}`);
|
|
30
|
+
exit(1);
|
|
31
|
+
}
|
|
32
|
+
// Actually upload the file
|
|
33
|
+
const { uploadUrl, publicUrl } = await res.json();
|
|
34
|
+
const uploadRes = await fetch(uploadUrl, {
|
|
35
|
+
method: "PUT",
|
|
36
|
+
headers: {
|
|
37
|
+
"Content-Type": contentType,
|
|
38
|
+
},
|
|
39
|
+
body: fileContents,
|
|
40
|
+
});
|
|
41
|
+
if (!uploadRes.ok) {
|
|
42
|
+
const errorText = await uploadRes.text();
|
|
43
|
+
console.error(`🚫 Upload failed: ${errorText}`);
|
|
44
|
+
exit(1);
|
|
45
|
+
}
|
|
46
|
+
return publicUrl;
|
|
47
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { build } from "esbuild";
|
|
2
|
+
export const buildComponent = async (file) => {
|
|
3
|
+
const res = await build({
|
|
4
|
+
entryPoints: [file],
|
|
5
|
+
bundle: true,
|
|
6
|
+
write: false,
|
|
7
|
+
minify: true,
|
|
8
|
+
sourcemap: "inline",
|
|
9
|
+
platform: "browser",
|
|
10
|
+
format: "cjs",
|
|
11
|
+
external: [
|
|
12
|
+
"@examplary/ui",
|
|
13
|
+
"react",
|
|
14
|
+
"react-dom",
|
|
15
|
+
"react/jsx-runtime",
|
|
16
|
+
"react-dom/client",
|
|
17
|
+
],
|
|
18
|
+
});
|
|
19
|
+
let js = res.outputFiles[0].text;
|
|
20
|
+
let sourcemap = "";
|
|
21
|
+
const sourceMapStatement = "\n//# sourceMappingURL=data:application/json;base64,";
|
|
22
|
+
if (js.includes(sourceMapStatement)) {
|
|
23
|
+
sourcemap = Buffer.from(js.split(sourceMapStatement)[1].trim(), "base64").toString("utf-8");
|
|
24
|
+
js = js.split(sourceMapStatement)[0].trim();
|
|
25
|
+
}
|
|
26
|
+
return { js, sourcemap };
|
|
27
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const deepCompileJsonata: (expression: any) => Promise<any>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import jsonata from "jsonata";
|
|
2
|
+
export const deepCompileJsonata = async (expression) => {
|
|
3
|
+
if (typeof expression === "string" && expression.startsWith("=")) {
|
|
4
|
+
try {
|
|
5
|
+
jsonata(expression.substring(1));
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
throw new Error(`JSONata evaluation failed for "${expression}": ${error.message}`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
if (Array.isArray(expression)) {
|
|
12
|
+
const output = [];
|
|
13
|
+
for (const o of expression) {
|
|
14
|
+
output.push(await deepCompileJsonata(o));
|
|
15
|
+
}
|
|
16
|
+
return output;
|
|
17
|
+
}
|
|
18
|
+
if (typeof expression === "object" && expression !== null) {
|
|
19
|
+
const result = {};
|
|
20
|
+
for (const [key, value] of Object.entries(expression)) {
|
|
21
|
+
result[key] = await deepCompileJsonata(value);
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
return expression;
|
|
26
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads and parses the question-type definition file.
|
|
3
|
+
* Prefers question-type.json over question-type.yml.
|
|
4
|
+
*/
|
|
5
|
+
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
|
+
};
|
|
15
|
+
path: string;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { cwd } from "process";
|
|
4
|
+
import yaml from "yaml";
|
|
5
|
+
/**
|
|
6
|
+
* Reads and parses the question-type definition file.
|
|
7
|
+
* Prefers question-type.json over question-type.yml.
|
|
8
|
+
*/
|
|
9
|
+
export const readMetaFile = async () => {
|
|
10
|
+
const dir = cwd();
|
|
11
|
+
const jsonPath = join(dir, "question-type.json");
|
|
12
|
+
const ymlPath = join(dir, "question-type.yml");
|
|
13
|
+
// Try JSON first, then YAML
|
|
14
|
+
for (const { path, parse, format } of [
|
|
15
|
+
{ path: jsonPath, parse: (v) => JSON.parse(v), format: "JSON" },
|
|
16
|
+
{ path: ymlPath, parse: (v) => yaml.parse(v), format: "YAML" },
|
|
17
|
+
]) {
|
|
18
|
+
let contents;
|
|
19
|
+
try {
|
|
20
|
+
contents = await readFile(path, "utf-8");
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const definition = parse(contents);
|
|
27
|
+
if (definition.$schema)
|
|
28
|
+
delete definition.$schema;
|
|
29
|
+
return { definition, path };
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
throw new Error(`Invalid ${format} in ${path}: ${error.message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
throw new Error("No question-type.json or question-type.yml found in the current directory");
|
|
36
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const buildStyles: () => Promise<string | null>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import tailwindcss from "@tailwindcss/postcss";
|
|
4
|
+
import postcss from "postcss";
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const tailwindDir = path.dirname(require.resolve("tailwindcss/package.json"));
|
|
7
|
+
export const buildStyles = async () => {
|
|
8
|
+
const absolutePath = path.resolve(tailwindDir);
|
|
9
|
+
const inputCSS = `
|
|
10
|
+
@import "${absolutePath}/theme.css" layer(theme);
|
|
11
|
+
@import "${absolutePath}/utilities.css" layer(utilities);
|
|
12
|
+
`;
|
|
13
|
+
const result = await postcss([
|
|
14
|
+
tailwindcss({
|
|
15
|
+
optimize: { minify: true },
|
|
16
|
+
}),
|
|
17
|
+
]).process(inputCSS, { from: "module-css.css" });
|
|
18
|
+
const utilitiesStyles = result.css.split("@layer utilities")[1].trim();
|
|
19
|
+
if (utilitiesStyles.length < 5) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const css = result.css.replace(/@layer /g, "@layer modules_");
|
|
23
|
+
return css;
|
|
24
|
+
};
|