@examplary/cli 1.15.0 → 1.17.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 (43) hide show
  1. package/README.md +21 -24
  2. package/dist/commands/init.d.ts +14 -0
  3. package/dist/commands/init.js +103 -0
  4. package/dist/commands/package.d.ts +1 -0
  5. package/dist/commands/package.js +16 -0
  6. package/dist/commands/publish.d.ts +5 -0
  7. package/dist/commands/publish.js +57 -0
  8. package/dist/commands/set-secret.d.ts +6 -0
  9. package/dist/commands/set-secret.js +29 -0
  10. package/dist/commands/validate.d.ts +0 -1
  11. package/dist/commands/validate.js +2 -20
  12. package/dist/index.js +38 -29
  13. package/dist/lib/api.d.ts +1 -1
  14. package/dist/lib/api.js +17 -6
  15. package/dist/lib/build-app.d.ts +21 -0
  16. package/dist/lib/build-app.js +179 -0
  17. package/dist/lib/build-current-app.d.ts +4 -0
  18. package/dist/lib/build-current-app.js +21 -0
  19. package/dist/lib/build-question-type.d.ts +8 -0
  20. package/dist/lib/build-question-type.js +48 -0
  21. package/dist/lib/bundle.d.ts +4 -3
  22. package/dist/lib/bundle.js +9 -31
  23. package/dist/lib/styles.d.ts +2 -1
  24. package/dist/lib/styles.js +3 -1
  25. package/dist/lib/validate.d.ts +2 -4
  26. package/dist/lib/validate.js +7 -26
  27. package/package.json +4 -24
  28. package/dist/commands/preview.d.ts +0 -5
  29. package/dist/commands/preview.js +0 -195
  30. package/dist/commands/upload.d.ts +0 -5
  31. package/dist/commands/upload.js +0 -125
  32. package/dist/lib/read-meta.d.ts +0 -20
  33. package/dist/lib/read-meta.js +0 -44
  34. package/dist/lib/safe-path.d.ts +0 -5
  35. package/dist/lib/safe-path.js +0 -13
  36. package/dist/preview-ui/assets/icon-nodes-CxaTPv-R.js +0 -2
  37. package/dist/preview-ui/assets/icon-nodes-CxaTPv-R.js.map +0 -1
  38. package/dist/preview-ui/assets/index-b38eDKFI.js +0 -3663
  39. package/dist/preview-ui/assets/index-b38eDKFI.js.map +0 -1
  40. package/dist/preview-ui/assets/index-boullcXq.css +0 -1
  41. package/dist/preview-ui/assets/tags-BUvo71XG.js +0 -2
  42. package/dist/preview-ui/assets/tags-BUvo71XG.js.map +0 -1
  43. package/dist/preview-ui/index.html +0 -21
@@ -1,195 +0,0 @@
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 { readJsonOrYamlFile, readMetaFile } from "../lib/read-meta.js";
15
- import { resolveInside } from "../lib/safe-path.js";
16
- import { buildStyles } from "../lib/styles.js";
17
- export const previewCommand = async (argv) => {
18
- setApiHost(argv.host);
19
- setApiKey(argv.key);
20
- const dir = cwd();
21
- // Verify a question-type file exists (json or yml)
22
- try {
23
- await readMetaFile();
24
- }
25
- catch (e) {
26
- console.error(`🚫 ${e.message}`);
27
- exit(1);
28
- }
29
- // Let's start a Vite server to preview the question type
30
- const root = resolve(import.meta.dirname, "..", "preview-ui");
31
- const app = new Hono();
32
- app.get("/api/bundle", async (c) => {
33
- const path = c.req.query("path");
34
- const sourcemap = c.req.query("sourcemap") === "1";
35
- const componentName = c.req.query("componentName");
36
- try {
37
- const componentOutput = await buildComponent(resolveInside(dir, path));
38
- if (sourcemap) {
39
- return c.body(componentOutput.sourcemap, 200, {
40
- "Content-Type": "application/json",
41
- });
42
- }
43
- else {
44
- const sizeInKb = componentOutput.js.length / 1000;
45
- if (sizeInKb > 100) {
46
- console.warn(`⚠️ Component ${componentName} is larger than 100 kB! Consider optimizing it.`);
47
- }
48
- let output = componentOutput.js;
49
- if (componentOutput.sourcemap) {
50
- output += `\n//# sourceMappingURL=/api/bundle?componentName=${componentName}&path=${encodeURIComponent(path || "")}&sourcemap=1`;
51
- }
52
- return c.body(output, 200, {
53
- "Content-Type": "application/javascript",
54
- });
55
- }
56
- }
57
- catch (error) {
58
- console.error(`🚫 Error bundling component ${componentName} from ${path}: ${error.message}`);
59
- return c.text(`Error bundling component ${componentName}: ${error.message}`, 500);
60
- }
61
- });
62
- app.get("/api/icon", async (c) => {
63
- const path = c.req.query("path") || "";
64
- let icon;
65
- try {
66
- icon = await readFile(resolveInside(dir, path), "utf-8");
67
- }
68
- catch (error) {
69
- return c.text(`Error reading icon from ${path}: ${error.message}`, 400);
70
- }
71
- return c.body(icon, 200, {
72
- "Content-Type": "image/svg+xml",
73
- });
74
- });
75
- app.get("/api/stylesheet", async (c) => {
76
- const componentName = c.req.query("componentName");
77
- try {
78
- const componentOutput = await buildStyles();
79
- return c.body(componentOutput || "", 200, {
80
- "Content-Type": "text/css",
81
- });
82
- }
83
- catch (error) {
84
- console.error(`🚫 Error building CSS for component ${componentName}: ${error.message}`);
85
- return c.text(`Error building CSS for component ${componentName}: ${error.message}`, 500);
86
- }
87
- });
88
- app.get("/__reload", async (c) => {
89
- if (wss) {
90
- wss.clients.forEach((client) => {
91
- if (client.readyState === 1) {
92
- // WebSocket.OPEN
93
- client.send(JSON.stringify({ type: "reload" }));
94
- }
95
- });
96
- }
97
- return c.body(null, 201);
98
- });
99
- let wsPort; // WebSocket port will be set when server starts
100
- app.get("/", async (c) => {
101
- let questionType;
102
- try {
103
- ({ definition: questionType } = await readMetaFile());
104
- questionType.components = questionType.components
105
- ? Object.entries(questionType.components).reduce((acc, [componentName, componentPath]) => ({
106
- ...acc,
107
- [componentName]: `/api/bundle?componentName=${componentName}&path=${encodeURIComponent(componentPath)}`,
108
- }), {})
109
- : questionType.components;
110
- }
111
- catch (e) {
112
- return c.text(`Error reading question type definition: ${e.message}`, 500);
113
- }
114
- // Default question/answer payloads for the preview, if a preview file exists
115
- let previewData = null;
116
- try {
117
- previewData = (await readJsonOrYamlFile(dir, "preview"))?.data ?? null;
118
- }
119
- catch (e) {
120
- console.warn(`⚠️ ${e.message}`);
121
- }
122
- let template = await readFile(resolve(root, "index.html"), "utf-8");
123
- // Inject hot reload script
124
- const hotReloadScript = `
125
- <script>
126
- (function() {
127
- const wsPort = ${wsPort || "null"};
128
- if (wsPort) {
129
- const ws = new WebSocket('ws://localhost:' + wsPort);
130
- ws.onmessage = function(event) {
131
- const data = JSON.parse(event.data);
132
- if (data.type === 'reload') {
133
- console.log('🔄 Reloading page due to file change...');
134
- window.location.reload();
135
- }
136
- };
137
- ws.onopen = function() {
138
- console.log('🔗 Hot reload connected');
139
- };
140
- ws.onclose = function() {
141
- console.log('🔌 Hot reload disconnected');
142
- };
143
- ws.onerror = function(error) {
144
- console.log('❌ Hot reload error:', error);
145
- };
146
- }
147
- })();
148
- </script>
149
- `;
150
- template =
151
- "<script>window._questionType = " +
152
- JSON.stringify(questionType) +
153
- ";window._previewData = " +
154
- JSON.stringify(previewData) +
155
- `;</script>${template}${hotReloadScript}`;
156
- return c.html(template);
157
- });
158
- app.use("/assets/*", serveStatic({ root }));
159
- // Set up file watcher
160
- const watchDir = cwd();
161
- console.log(`📁 Watching ${watchDir} for changes...`);
162
- let wss; // Will be initialized after server starts
163
- const watcher = watch(watchDir, { recursive: true }, (filename) => {
164
- if (filename && !filename.startsWith(".")) {
165
- // Broadcast reload message to all connected clients
166
- if (wss) {
167
- wss.clients.forEach((client) => {
168
- if (client.readyState === 1) {
169
- // WebSocket.OPEN
170
- client.send(JSON.stringify({ type: "reload" }));
171
- }
172
- });
173
- }
174
- }
175
- });
176
- // Clean up watcher on process exit
177
- process.on("SIGINT", () => {
178
- watcher.close();
179
- process.exit(0);
180
- });
181
- serve({
182
- fetch: app.fetch,
183
- hostname: "localhost",
184
- port: 5777,
185
- }, (address) => {
186
- const port = address.port;
187
- wsPort = port + 1234;
188
- const wsServer = createHttpServer();
189
- wss = new WebSocketServer({ server: wsServer });
190
- wsServer.listen(wsPort);
191
- const url = `http://localhost:${port}`;
192
- console.log(`🚀 Preview server running at ${url}`);
193
- open(url);
194
- });
195
- };
@@ -1,5 +0,0 @@
1
- #! /usr/bin/env node
2
- export declare const uploadCommand: (argv: {
3
- host: string;
4
- key: string;
5
- }) => Promise<void>;
@@ -1,125 +0,0 @@
1
- #! /usr/bin/env node
2
- import { readdir, readFile } from "fs/promises";
3
- import { join } from "path";
4
- import { cwd, exit } from "process";
5
- import { setApiHost, setApiKey, uploadFile } from "../lib/api.js";
6
- import { readJsonOrYamlFile, readMetaFile } from "../lib/read-meta.js";
7
- import { isValid, printValidationResult, validateDefinition, } from "../lib/validate.js";
8
- /**
9
- * Uploads e.g. `README.md` (English) and its translations like `README.nl.md`,
10
- * returning their URLs keyed by language code.
11
- */
12
- const uploadLocalizedFiles = async (dir, baseName, extension, contentType) => {
13
- const pattern = new RegExp(`^${baseName}(?:\\.([a-z]{2}))?\\.${extension}$`);
14
- const urls = {};
15
- for (const file of await readdir(dir)) {
16
- const match = file.match(pattern);
17
- if (!match)
18
- continue;
19
- const contents = await readFile(join(dir, file));
20
- if (!contents.length)
21
- continue;
22
- urls[match[1] || "en"] = await uploadFile(file, contents, contentType);
23
- console.log(`• Uploaded ${file}`);
24
- }
25
- return Object.keys(urls).length ? urls : undefined;
26
- };
27
- export const uploadCommand = async (argv) => {
28
- setApiHost(argv.host);
29
- setApiKey(argv.key);
30
- const dir = cwd();
31
- let definition = {};
32
- try {
33
- ({ definition } = await readMetaFile());
34
- }
35
- catch (e) {
36
- console.error(`🚫 ${e.message}`);
37
- exit(1);
38
- }
39
- console.log(`Uploading question type: ${definition.id}...`);
40
- // Run all checks (incl. response processing test cases) before uploading
41
- const result = await validateDefinition(definition, dir);
42
- printValidationResult(result);
43
- if (!isValid(result)) {
44
- console.error(`\n🚫 Validation failed, aborting upload`);
45
- exit(1);
46
- }
47
- // Upload the bundled components produced during validation
48
- const components = {};
49
- for (const [componentType, output] of Object.entries(result.bundles)) {
50
- // Upload source map first if it exists
51
- let sourceMapStatement = "";
52
- if (output.sourcemap) {
53
- const url = await uploadFile(`component-${componentType}.js.map`, output.sourcemap, "application/json");
54
- sourceMapStatement = `\n//# sourceMappingURL=${url}`;
55
- }
56
- components[componentType] = await uploadFile(`component-${componentType}.js`, output.js + sourceMapStatement, "application/javascript");
57
- console.log(`• Uploaded component ${componentType}`);
58
- }
59
- definition.components = components;
60
- // Upload the bundled functions
61
- const functions = {};
62
- for (const [functionName, output] of Object.entries(result.functions)) {
63
- // Upload source map first if it exists
64
- let sourceMapStatement = "";
65
- if (output.sourcemap) {
66
- const url = await uploadFile(`function-${functionName}.js.map`, output.sourcemap, "application/json");
67
- sourceMapStatement = `\n//# sourceMappingURL=${url}`;
68
- }
69
- functions[functionName] = await uploadFile(`function-${functionName}.js`, output.js + sourceMapStatement, "application/javascript");
70
- console.log(`• Uploaded function ${functionName}`);
71
- }
72
- definition.functions = functions;
73
- // Upload styles if present
74
- if (result.styles) {
75
- definition.stylesheet = await uploadFile("styles.css", result.styles, "text/css");
76
- const sizeInKb = (result.styles.length / 1000).toFixed(2);
77
- console.log(`• Uploaded styles (${sizeInKb} kB)`);
78
- }
79
- // Upload icon if specified
80
- if (definition.icon && result.iconContents) {
81
- definition.icon = await uploadFile("icon.svg", result.iconContents, "image/svg+xml");
82
- console.log(`• Uploaded icon`);
83
- }
84
- definition.readmeUrls = await uploadLocalizedFiles(dir, "README", "md", "text/markdown");
85
- definition.readme = definition.readmeUrls?.en;
86
- definition.previewImageUrls = await uploadLocalizedFiles(dir, "preview", "png", "image/png");
87
- definition.previewImage = definition.previewImageUrls?.en;
88
- // Upload the preview data if present, always converted to JSON
89
- try {
90
- const preview = await readJsonOrYamlFile(dir, "preview");
91
- if (preview) {
92
- definition.previewData = await uploadFile("preview.json", JSON.stringify(preview.data), "application/json");
93
- console.log(`• Uploaded preview data`);
94
- }
95
- }
96
- catch (e) {
97
- console.error(`🚫 ${e.message}`);
98
- exit(1);
99
- }
100
- // Test cases have already run during validation, no need to store them
101
- if (definition.responseProcessing?.testCases) {
102
- delete definition.responseProcessing?.testCases;
103
- }
104
- // Upload the main question type definition
105
- const res = await fetch(`${argv.host}/question-types`, {
106
- headers: {
107
- Authorization: `Bearer ${argv.key}`,
108
- "Content-Type": "application/json",
109
- },
110
- method: "POST",
111
- body: JSON.stringify(definition),
112
- });
113
- if (!res.ok) {
114
- console.error(`🚫 Uploading question type failed`);
115
- try {
116
- const errorResponse = await res.json();
117
- console.log(errorResponse);
118
- }
119
- catch (e) {
120
- console.log(e);
121
- }
122
- exit(1);
123
- }
124
- console.log(`• Finished uploading definition\n`);
125
- };
@@ -1,20 +0,0 @@
1
- import type { QuestionType } from "@examplary/schemas";
2
- export type QuestionTypeDefinition = Partial<QuestionType> & {
3
- $schema?: string;
4
- };
5
- /**
6
- * Reads and parses a `<baseName>.json` or `<baseName>.yml` file from the
7
- * given directory, preferring JSON over YAML. Returns null if neither exists.
8
- */
9
- export declare const readJsonOrYamlFile: (dir: string, baseName: string) => Promise<{
10
- data: any;
11
- path: string;
12
- } | null>;
13
- /**
14
- * Reads and parses the question-type definition file.
15
- * Prefers question-type.json over question-type.yml.
16
- */
17
- export declare const readMetaFile: () => Promise<{
18
- definition: QuestionTypeDefinition;
19
- path: string;
20
- }>;
@@ -1,44 +0,0 @@
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 a `<baseName>.json` or `<baseName>.yml` file from the
7
- * given directory, preferring JSON over YAML. Returns null if neither exists.
8
- */
9
- export const readJsonOrYamlFile = async (dir, baseName) => {
10
- for (const { ext, parse, format } of [
11
- { ext: "json", parse: (v) => JSON.parse(v), format: "JSON" },
12
- { ext: "yml", parse: (v) => yaml.parse(v), format: "YAML" },
13
- ]) {
14
- const path = join(dir, `${baseName}.${ext}`);
15
- let contents;
16
- try {
17
- contents = await readFile(path, "utf-8");
18
- }
19
- catch {
20
- continue;
21
- }
22
- try {
23
- return { data: parse(contents), path };
24
- }
25
- catch (error) {
26
- throw new Error(`Invalid ${format} in ${path}: ${error.message}`);
27
- }
28
- }
29
- return null;
30
- };
31
- /**
32
- * Reads and parses the question-type definition file.
33
- * Prefers question-type.json over question-type.yml.
34
- */
35
- export const readMetaFile = async () => {
36
- const result = await readJsonOrYamlFile(cwd(), "question-type");
37
- if (!result) {
38
- throw new Error("No question-type.json or question-type.yml found in the current directory");
39
- }
40
- const definition = result.data;
41
- if (definition.$schema)
42
- delete definition.$schema;
43
- return { definition, path: result.path };
44
- };
@@ -1,5 +0,0 @@
1
- /**
2
- * Resolves `path` against `dir` and throws if the result escapes `dir`,
3
- * preventing path traversal via `../` segments or absolute paths.
4
- */
5
- export declare const resolveInside: (dir: string, path: string) => string;
@@ -1,13 +0,0 @@
1
- import { resolve, sep } from "path";
2
- /**
3
- * Resolves `path` against `dir` and throws if the result escapes `dir`,
4
- * preventing path traversal via `../` segments or absolute paths.
5
- */
6
- export const resolveInside = (dir, path) => {
7
- const base = resolve(dir);
8
- const resolved = resolve(base, path);
9
- if (resolved !== base && !resolved.startsWith(base + sep)) {
10
- throw new Error(`Path "${path}" is outside of the question type directory`);
11
- }
12
- return resolved;
13
- };